vanta-admin 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {vanta_admin-0.2.0/vanta_admin.egg-info → vanta_admin-0.3.0}/PKG-INFO +4 -2
  2. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/README.md +3 -1
  3. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/pyproject.toml +1 -1
  4. vanta_admin-0.3.0/vanta_admin/static/admin/js/changelist.js +120 -0
  5. vanta_admin-0.3.0/vanta_admin/static/admin/js/forms.js +189 -0
  6. vanta_admin-0.3.0/vanta_admin/static/admin/js/messages.js +73 -0
  7. vanta_admin-0.3.0/vanta_admin/static/admin/js/sidebar.js +167 -0
  8. vanta_admin-0.3.0/vanta_admin/static/admin/js/theme.js +37 -0
  9. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/base_site.html +3 -0
  10. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/change_list.html +1 -0
  11. {vanta_admin-0.2.0 → vanta_admin-0.3.0/vanta_admin.egg-info}/PKG-INFO +4 -2
  12. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin.egg-info/SOURCES.txt +4 -0
  13. vanta_admin-0.2.0/vanta_admin/static/admin/js/sidebar.js +0 -563
  14. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/LICENSE +0 -0
  15. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/MANIFEST.in +0 -0
  16. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/setup.cfg +0 -0
  17. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/__init__.py +0 -0
  18. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/apps.py +0 -0
  19. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/css/theme.admin.css +0 -0
  20. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/css/theme.base.css +0 -0
  21. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/css/theme.css +0 -0
  22. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/css/theme.login.css +0 -0
  23. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/css/theme.responsive.css +0 -0
  24. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/css/theme.tokens.css +0 -0
  25. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/css/two_factor.css +0 -0
  26. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/img/favicon.png +0 -0
  27. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/app_index.html +0 -0
  28. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/base.html +0 -0
  29. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/change_form.html +0 -0
  30. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/filter.html +0 -0
  31. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/includes/breadcrumbs.html +0 -0
  32. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/index.html +0 -0
  33. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/login.html +0 -0
  34. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/nav_sidebar.html +0 -0
  35. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/two_factor/_base.html +0 -0
  36. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/two_factor/_base_focus.html +0 -0
  37. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/two_factor/_wizard_actions.html +0 -0
  38. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/two_factor/core/login.html +0 -0
  39. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/two_factor/core/setup_complete.html +0 -0
  40. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/two_factor/profile/profile.html +0 -0
  41. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templatetags/__init__.py +0 -0
  42. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templatetags/admin_theme.py +0 -0
  43. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin.egg-info/dependency_links.txt +0 -0
  44. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin.egg-info/requires.txt +0 -0
  45. {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vanta-admin
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A sharp, dark, and portable Django admin theme forged for builders who want the admin to feel less default.
5
5
  Author: oli-dev0
6
6
  License-Expression: MIT
@@ -28,7 +28,9 @@ Vanta Admin is a Django admin theme with a darker interface, tighter density,
28
28
  and a standalone structure that stays close to Django's admin template system.
29
29
 
30
30
  It is designed to be portable and to avoid frontend runtime dependencies outside
31
- standard Django admin behavior.
31
+ standard Django admin behavior. You still use Django admin; Vanta changes the
32
+ shell, navigation, common controls, messages, and visual treatment so the admin
33
+ feels more deliberate in everyday use.
32
34
 
33
35
  ## What It Adds
34
36
 
@@ -3,7 +3,9 @@ Vanta Admin is a Django admin theme with a darker interface, tighter density,
3
3
  and a standalone structure that stays close to Django's admin template system.
4
4
 
5
5
  It is designed to be portable and to avoid frontend runtime dependencies outside
6
- standard Django admin behavior.
6
+ standard Django admin behavior. You still use Django admin; Vanta changes the
7
+ shell, navigation, common controls, messages, and visual treatment so the admin
8
+ feels more deliberate in everyday use.
7
9
 
8
10
  ## What It Adds
9
11
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vanta-admin"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "A sharp, dark, and portable Django admin theme forged for builders who want the admin to feel less default."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ {
4
+ const filterSectionsToggle = document.getElementById('admin-filter-sections-toggle');
5
+ const filterSections = Array.from(document.querySelectorAll('#changelist-filter details'));
6
+
7
+ function isDetailsOpen(section) {
8
+ return section.open;
9
+ }
10
+
11
+ function updateFilterSectionsToggle() {
12
+ if (!filterSectionsToggle) {
13
+ return;
14
+ }
15
+
16
+ const allOpen = filterSections.length > 0 && filterSections.every(isDetailsOpen);
17
+ const label = allOpen ? 'Collapse all filter sections' : 'Expand all filter sections';
18
+
19
+ filterSectionsToggle.setAttribute('aria-label', label);
20
+ filterSectionsToggle.setAttribute('title', label);
21
+ filterSectionsToggle.classList.toggle('is-collapse-mode', allOpen);
22
+ }
23
+
24
+ function setFilterSectionOpen(section, isOpen) {
25
+ section.open = isOpen;
26
+ }
27
+
28
+ function syncChangelistActions() {
29
+ const form = document.getElementById('changelist-form');
30
+ const actions = Array.from(document.querySelectorAll('#changelist .actions'));
31
+
32
+ if (!form || actions.length === 0) {
33
+ return;
34
+ }
35
+
36
+ const checkboxes = Array.from(form.querySelectorAll('input.action-select[type="checkbox"]'));
37
+ const primaryActionBar = actions[0];
38
+
39
+ function syncStickyActionBar() {
40
+ if (!primaryActionBar || !form.classList.contains('has-selected-actions')) {
41
+ form.classList.remove('has-sticky-actions');
42
+ return;
43
+ }
44
+
45
+ const formRect = form.getBoundingClientRect();
46
+ const contentBox = form.closest('.changelist-form-container > div') || form.parentElement || form;
47
+ const contentRect = contentBox.getBoundingClientRect();
48
+ const shouldStick = formRect.top < 12 && formRect.bottom > 84;
49
+ form.classList.toggle('has-sticky-actions', shouldStick);
50
+
51
+ if (!shouldStick) {
52
+ return;
53
+ }
54
+
55
+ const left = Math.max(contentRect.left + 12, 12);
56
+ const rightEdge = Math.min(contentRect.right - 12, window.innerWidth - 12);
57
+ const width = Math.max(rightEdge - left, 0);
58
+ primaryActionBar.style.setProperty('--admin-actions-fixed-left', `${left}px`);
59
+ primaryActionBar.style.setProperty('--admin-actions-fixed-width', `${width}px`);
60
+ }
61
+
62
+ actions.forEach((actionBar, index) => {
63
+ actionBar.classList.toggle('admin-actions--duplicate', index > 0);
64
+
65
+ if (index === 0 && !actionBar.querySelector('.admin-actions__clear')) {
66
+ const clearButton = document.createElement('button');
67
+ clearButton.type = 'button';
68
+ clearButton.className = 'admin-actions__clear';
69
+ clearButton.setAttribute('aria-label', 'Clear selected rows');
70
+ clearButton.addEventListener('click', () => {
71
+ checkboxes.forEach((checkbox) => {
72
+ checkbox.checked = false;
73
+ checkbox.closest('tr')?.classList.remove('selected');
74
+ checkbox.dispatchEvent(new Event('change', { bubbles: true }));
75
+ });
76
+ const actionToggle = form.querySelector('#action-toggle');
77
+ if (actionToggle) {
78
+ actionToggle.checked = false;
79
+ }
80
+ updateActionsVisibility();
81
+ });
82
+ actionBar.prepend(clearButton);
83
+ }
84
+ });
85
+
86
+ function updateActionsVisibility() {
87
+ const hasSelectedRows = checkboxes.some((checkbox) => checkbox.checked);
88
+ form.classList.toggle('has-selected-actions', hasSelectedRows);
89
+ actions.forEach((actionBar) => {
90
+ actionBar.setAttribute('aria-hidden', String(!hasSelectedRows));
91
+ });
92
+ syncStickyActionBar();
93
+ }
94
+
95
+ form.addEventListener('change', (event) => {
96
+ if (event.target.matches('input[type="checkbox"]')) {
97
+ updateActionsVisibility();
98
+ }
99
+ });
100
+ window.addEventListener('scroll', syncStickyActionBar, { passive: true });
101
+ window.addEventListener('resize', syncStickyActionBar);
102
+ updateActionsVisibility();
103
+ }
104
+
105
+ syncChangelistActions();
106
+
107
+ if (filterSectionsToggle) {
108
+ filterSectionsToggle.addEventListener('click', () => {
109
+ const allOpen = filterSections.length > 0 && filterSections.every(isDetailsOpen);
110
+ filterSections.forEach((section) => setFilterSectionOpen(section, !allOpen));
111
+ updateFilterSectionsToggle();
112
+ });
113
+ }
114
+
115
+ filterSections.forEach((section) => {
116
+ section.addEventListener('toggle', updateFilterSectionsToggle);
117
+ });
118
+
119
+ updateFilterSectionsToggle();
120
+ }
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ {
4
+ const customSelects = Array.from(document.querySelectorAll('select')).filter((select) => (
5
+ !select.multiple
6
+ && (!select.size || select.size <= 1)
7
+ && select.dataset.vantaEnhanced !== 'true'
8
+ ));
9
+
10
+ function enhanceActionSelect(select, index) {
11
+ const label = select.closest('label');
12
+ if (select.dataset.vantaEnhanced === 'true') {
13
+ return null;
14
+ }
15
+
16
+ const options = Array.from(select.options);
17
+ const isActionSelect = select.matches('#changelist .actions select[name="action"]');
18
+ const insertAfter = label && label.contains(select) ? label : select;
19
+ const dropdown = document.createElement('div');
20
+ const button = document.createElement('button');
21
+ const buttonText = document.createElement('span');
22
+ const menu = document.createElement('div');
23
+ const menuId = `admin-action-select-menu-${index}`;
24
+ const optionButtons = [];
25
+ const isRequired = select.required;
26
+
27
+ select.dataset.vantaEnhanced = 'true';
28
+ select.classList.add('admin-action-select__native');
29
+ select.tabIndex = -1;
30
+ select.setAttribute('aria-hidden', 'true');
31
+ select.required = false;
32
+ if (label) {
33
+ label.classList.add('admin-action-select__label');
34
+ }
35
+
36
+ dropdown.className = isActionSelect
37
+ ? 'admin-action-select'
38
+ : 'admin-action-select admin-action-select--field';
39
+ button.type = 'button';
40
+ button.className = 'admin-action-select__button';
41
+ button.disabled = select.disabled;
42
+ button.setAttribute('aria-haspopup', 'listbox');
43
+ button.setAttribute('aria-expanded', 'false');
44
+ button.setAttribute('aria-controls', menuId);
45
+ buttonText.className = 'admin-action-select__button-text';
46
+ button.append(buttonText);
47
+
48
+ menu.id = menuId;
49
+ menu.className = 'admin-action-select__menu';
50
+ menu.setAttribute('role', 'listbox');
51
+ menu.setAttribute('tabindex', '-1');
52
+
53
+ function getSelectedOption() {
54
+ return select.selectedOptions[0] || options[0];
55
+ }
56
+
57
+ function setOpen(isOpen) {
58
+ dropdown.classList.toggle('is-open', isOpen);
59
+ button.setAttribute('aria-expanded', String(isOpen));
60
+
61
+ if (isOpen) {
62
+ const selectedButton = optionButtons.find((optionButton) => optionButton.dataset.value === select.value);
63
+ window.requestAnimationFrame(() => {
64
+ (selectedButton || optionButtons[0])?.focus();
65
+ });
66
+ }
67
+ }
68
+
69
+ function syncButtonLabel() {
70
+ const selectedOption = getSelectedOption();
71
+ buttonText.textContent = selectedOption ? selectedOption.textContent.trim() : '';
72
+ button.setAttribute('aria-invalid', String(dropdown.classList.contains('has-error')));
73
+ optionButtons.forEach((optionButton) => {
74
+ const isSelected = optionButton.dataset.value === select.value;
75
+ optionButton.classList.toggle('is-selected', isSelected);
76
+ optionButton.setAttribute('aria-selected', String(isSelected));
77
+ });
78
+ }
79
+
80
+ function chooseOption(optionButton, shouldClose = true) {
81
+ select.value = optionButton.dataset.value;
82
+ select.dispatchEvent(new Event('change', { bubbles: true }));
83
+ dropdown.classList.remove('has-error');
84
+ syncButtonLabel();
85
+
86
+ if (shouldClose) {
87
+ setOpen(false);
88
+ button.focus();
89
+ }
90
+ }
91
+
92
+ options.forEach((option) => {
93
+ const optionButton = document.createElement('button');
94
+ optionButton.type = 'button';
95
+ optionButton.className = 'admin-action-select__option';
96
+ optionButton.dataset.value = option.value;
97
+ optionButton.textContent = option.textContent.trim();
98
+ optionButton.setAttribute('role', 'option');
99
+ optionButton.setAttribute('aria-selected', String(option.selected));
100
+ optionButton.disabled = option.disabled;
101
+
102
+ optionButton.addEventListener('click', () => chooseOption(optionButton));
103
+ optionButton.addEventListener('keydown', (event) => {
104
+ const currentIndex = optionButtons.indexOf(optionButton);
105
+ const lastIndex = optionButtons.length - 1;
106
+
107
+ if (event.key === 'ArrowDown') {
108
+ event.preventDefault();
109
+ optionButtons[Math.min(currentIndex + 1, lastIndex)]?.focus();
110
+ } else if (event.key === 'ArrowUp') {
111
+ event.preventDefault();
112
+ optionButtons[Math.max(currentIndex - 1, 0)]?.focus();
113
+ } else if (event.key === 'Home') {
114
+ event.preventDefault();
115
+ optionButtons[0]?.focus();
116
+ } else if (event.key === 'End') {
117
+ event.preventDefault();
118
+ optionButtons[lastIndex]?.focus();
119
+ } else if (event.key === 'Enter' || event.key === ' ') {
120
+ event.preventDefault();
121
+ chooseOption(optionButton);
122
+ } else if (event.key === 'Escape') {
123
+ event.preventDefault();
124
+ setOpen(false);
125
+ button.focus();
126
+ }
127
+ });
128
+
129
+ optionButtons.push(optionButton);
130
+ menu.append(optionButton);
131
+ });
132
+
133
+ button.addEventListener('click', () => {
134
+ setOpen(!dropdown.classList.contains('is-open'));
135
+ });
136
+
137
+ button.addEventListener('keydown', (event) => {
138
+ if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
139
+ event.preventDefault();
140
+ setOpen(true);
141
+ } else if (event.key === 'ArrowUp') {
142
+ event.preventDefault();
143
+ setOpen(true);
144
+ window.requestAnimationFrame(() => optionButtons.at(-1)?.focus());
145
+ } else if (event.key === 'Escape') {
146
+ setOpen(false);
147
+ }
148
+ });
149
+
150
+ select.addEventListener('change', syncButtonLabel);
151
+ select.form?.addEventListener('submit', (event) => {
152
+ if (isRequired && !select.value) {
153
+ event.preventDefault();
154
+ dropdown.classList.add('has-error');
155
+ syncButtonLabel();
156
+ setOpen(true);
157
+ button.focus();
158
+ }
159
+ });
160
+
161
+ dropdown.append(button, menu);
162
+ insertAfter.after(dropdown);
163
+ syncButtonLabel();
164
+
165
+ return dropdown;
166
+ }
167
+
168
+ const customDropdowns = customSelects
169
+ .map(enhanceActionSelect)
170
+ .filter(Boolean);
171
+
172
+ document.addEventListener('keydown', (event) => {
173
+ if (event.key === 'Escape') {
174
+ customDropdowns.forEach((dropdown) => {
175
+ dropdown.classList.remove('is-open');
176
+ dropdown.querySelector('.admin-action-select__button')?.setAttribute('aria-expanded', 'false');
177
+ });
178
+ }
179
+ });
180
+
181
+ document.addEventListener('click', (event) => {
182
+ customDropdowns.forEach((dropdown) => {
183
+ if (!dropdown.contains(event.target)) {
184
+ dropdown.classList.remove('is-open');
185
+ dropdown.querySelector('.admin-action-select__button')?.setAttribute('aria-expanded', 'false');
186
+ }
187
+ });
188
+ });
189
+ }
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ {
4
+ const adminMessages = Array.from(document.querySelectorAll('ul.messagelist li'));
5
+ const adminMessageDuration = 4000;
6
+
7
+ function dismissAdminMessage(message) {
8
+ window.clearTimeout(message.adminDismissTimer);
9
+ message.adminDismissTimer = null;
10
+ message.classList.add('is-removing');
11
+ window.setTimeout(() => {
12
+ const messageList = message.closest('ul.messagelist');
13
+ message.remove();
14
+ if (messageList && messageList.children.length === 0) {
15
+ messageList.remove();
16
+ }
17
+ }, 180);
18
+ }
19
+
20
+ function setAdminMessageTimer(message, duration) {
21
+ window.clearTimeout(message.adminDismissTimer);
22
+ message.adminTimerStartedAt = Date.now();
23
+ message.adminTimerRemaining = duration;
24
+ message.style.transition = `background-size ${duration}ms linear, opacity 160ms ease, transform 160ms ease`;
25
+ message.style.backgroundSize = '100% 100%';
26
+ message.adminDismissTimer = window.setTimeout(() => dismissAdminMessage(message), duration);
27
+ }
28
+
29
+ function pauseAdminMessageTimer(message) {
30
+ if (!message.adminDismissTimer || message.classList.contains('is-removing')) {
31
+ return;
32
+ }
33
+
34
+ const elapsed = Date.now() - message.adminTimerStartedAt;
35
+ message.adminTimerRemaining = Math.max(0, message.adminTimerRemaining - elapsed);
36
+ window.clearTimeout(message.adminDismissTimer);
37
+ message.adminDismissTimer = null;
38
+ message.style.backgroundSize = window.getComputedStyle(message).backgroundSize;
39
+ message.style.transition = 'opacity 160ms ease, transform 160ms ease';
40
+ }
41
+
42
+ function resumeAdminMessageTimer(message) {
43
+ if (message.adminDismissTimer || message.classList.contains('is-removing')) {
44
+ return;
45
+ }
46
+
47
+ if (message.adminTimerRemaining <= 0) {
48
+ dismissAdminMessage(message);
49
+ return;
50
+ }
51
+
52
+ window.requestAnimationFrame(() => {
53
+ setAdminMessageTimer(message, message.adminTimerRemaining);
54
+ });
55
+ }
56
+
57
+ adminMessages.forEach((message) => {
58
+ message.classList.remove('is-counting', 'is-removing');
59
+ message.style.transition = 'none';
60
+ message.style.backgroundSize = '0 100%';
61
+ message.offsetWidth;
62
+ message.addEventListener('mouseenter', () => pauseAdminMessageTimer(message));
63
+ message.addEventListener('mouseleave', () => resumeAdminMessageTimer(message));
64
+
65
+ window.requestAnimationFrame(() => {
66
+ message.style.transition = `background-size ${adminMessageDuration}ms linear, opacity 160ms ease, transform 160ms ease`;
67
+ window.requestAnimationFrame(() => {
68
+ message.classList.add('is-counting');
69
+ setAdminMessageTimer(message, adminMessageDuration);
70
+ });
71
+ });
72
+ });
73
+ }
@@ -0,0 +1,167 @@
1
+ 'use strict';
2
+
3
+ {
4
+ const sidebarStateKey = 'django.admin.theme.sidebar.isCollapsed';
5
+ const navSectionsStateKey = 'django.admin.theme.sidebar.openSections';
6
+
7
+ const body = document.body;
8
+ const root = document.documentElement;
9
+ const sidebar = document.getElementById('nav-sidebar');
10
+ const collapseButton = document.getElementById('admin-sidebar-collapse');
11
+ const menuButton = document.getElementById('admin-sidebar-menu-button');
12
+ const accountDetails = document.querySelector('.admin-sidebar__account');
13
+ const mobileQuery = window.matchMedia('(max-width: 1024px)');
14
+ const navSections = Array.from(document.querySelectorAll('[data-admin-nav-section]'));
15
+
16
+ function setCollapsed(isCollapsed, shouldPersist) {
17
+ body.classList.toggle('admin-sidebar-collapsed', isCollapsed);
18
+
19
+ if (collapseButton) {
20
+ collapseButton.setAttribute('aria-expanded', String(!isCollapsed));
21
+ const label = isCollapsed ? 'Expand sidebar' : 'Collapse sidebar';
22
+ collapseButton.setAttribute('aria-label', label);
23
+ const text = collapseButton.querySelector('.admin-sidebar__collapse-text');
24
+ if (text) {
25
+ text.textContent = label;
26
+ }
27
+ }
28
+
29
+ if (shouldPersist) {
30
+ localStorage.setItem(sidebarStateKey, String(isCollapsed));
31
+ }
32
+ }
33
+
34
+ function setMobileOpen(isOpen) {
35
+ body.classList.toggle('admin-sidebar-mobile-open', isOpen);
36
+
37
+ if (menuButton) {
38
+ menuButton.setAttribute('aria-expanded', String(isOpen));
39
+ menuButton.setAttribute('aria-label', isOpen ? 'Close admin navigation' : 'Open admin navigation');
40
+ }
41
+
42
+ syncMobileVisibility();
43
+ }
44
+
45
+ function syncMobileVisibility() {
46
+ if (sidebar) {
47
+ const isHiddenOnMobile = !body.classList.contains('admin-sidebar-mobile-open') && mobileQuery.matches;
48
+ sidebar.setAttribute('aria-hidden', String(isHiddenOnMobile));
49
+ sidebar.toggleAttribute('inert', isHiddenOnMobile);
50
+ }
51
+ }
52
+
53
+ function isSectionOpen(section) {
54
+ return section.hasAttribute('open');
55
+ }
56
+
57
+ function getSectionKey(section) {
58
+ return section.dataset.adminNavSection;
59
+ }
60
+
61
+ function syncSectionAria(section) {
62
+ const summary = section.querySelector('.admin-sidebar__group-title');
63
+ if (summary) {
64
+ summary.setAttribute('aria-expanded', String(isSectionOpen(section)));
65
+ }
66
+ }
67
+
68
+ function getStoredOpenSections() {
69
+ try {
70
+ const value = JSON.parse(localStorage.getItem(navSectionsStateKey));
71
+ return Array.isArray(value) ? new Set(value) : null;
72
+ } catch (error) {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ function persistOpenSections() {
78
+ const openSections = navSections
79
+ .filter(isSectionOpen)
80
+ .map(getSectionKey)
81
+ .filter(Boolean);
82
+
83
+ localStorage.setItem(navSectionsStateKey, JSON.stringify(openSections));
84
+ }
85
+
86
+ function applyStoredOpenSections() {
87
+ const storedOpenSections = getStoredOpenSections();
88
+
89
+ if (!storedOpenSections) {
90
+ root.classList.remove('admin-nav-state-pending');
91
+ return;
92
+ }
93
+
94
+ navSections.forEach((section) => {
95
+ const sectionKey = getSectionKey(section);
96
+ if (!sectionKey) {
97
+ return;
98
+ }
99
+
100
+ section.open = storedOpenSections.has(sectionKey);
101
+ syncSectionAria(section);
102
+ });
103
+
104
+ root.classList.remove('admin-nav-state-pending');
105
+ }
106
+
107
+ const storedCollapsed = localStorage.getItem(sidebarStateKey);
108
+ setCollapsed(storedCollapsed === 'true', false);
109
+ setMobileOpen(false);
110
+ applyStoredOpenSections();
111
+
112
+ if (collapseButton) {
113
+ collapseButton.addEventListener('click', () => {
114
+ if (mobileQuery.matches) {
115
+ setMobileOpen(false);
116
+ return;
117
+ }
118
+
119
+ setCollapsed(!body.classList.contains('admin-sidebar-collapsed'), true);
120
+ });
121
+ }
122
+
123
+ if (menuButton) {
124
+ menuButton.addEventListener('click', () => {
125
+ setMobileOpen(!body.classList.contains('admin-sidebar-mobile-open'));
126
+ });
127
+ }
128
+
129
+ navSections.forEach((section) => {
130
+ syncSectionAria(section);
131
+ section.addEventListener('toggle', () => {
132
+ syncSectionAria(section);
133
+ persistOpenSections();
134
+ });
135
+ });
136
+
137
+ document.addEventListener('keydown', (event) => {
138
+ if (event.key === 'Escape') {
139
+ setMobileOpen(false);
140
+ if (accountDetails) {
141
+ accountDetails.open = false;
142
+ }
143
+ }
144
+ });
145
+
146
+ document.addEventListener('click', (event) => {
147
+ if (
148
+ accountDetails
149
+ && accountDetails.open
150
+ && !accountDetails.contains(event.target)
151
+ ) {
152
+ accountDetails.open = false;
153
+ }
154
+
155
+ if (
156
+ body.classList.contains('admin-sidebar-mobile-open')
157
+ && sidebar
158
+ && !sidebar.contains(event.target)
159
+ && menuButton
160
+ && !menuButton.contains(event.target)
161
+ ) {
162
+ setMobileOpen(false);
163
+ }
164
+ });
165
+
166
+ mobileQuery.addEventListener('change', syncMobileVisibility);
167
+ }
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ {
4
+ const validThemeValues = new Set(['light', 'dark']);
5
+ const root = document.documentElement;
6
+ const themeButtons = document.querySelectorAll('[data-admin-theme-value]');
7
+
8
+ function updateThemeButtons(theme) {
9
+ themeButtons.forEach((button) => {
10
+ button.setAttribute('aria-pressed', String(button.dataset.adminThemeValue === theme));
11
+ });
12
+ }
13
+
14
+ function applyTheme(theme) {
15
+ if (!validThemeValues.has(theme)) {
16
+ return;
17
+ }
18
+
19
+ root.dataset.theme = theme;
20
+ localStorage.setItem('theme', theme);
21
+ updateThemeButtons(theme);
22
+ }
23
+
24
+ const storedTheme = localStorage.getItem('theme');
25
+ if (validThemeValues.has(storedTheme)) {
26
+ root.dataset.theme = storedTheme;
27
+ updateThemeButtons(storedTheme);
28
+ } else {
29
+ updateThemeButtons(root.dataset.theme);
30
+ }
31
+
32
+ themeButtons.forEach((button) => {
33
+ button.addEventListener('click', () => {
34
+ applyTheme(button.dataset.adminThemeValue);
35
+ });
36
+ });
37
+ }
@@ -25,7 +25,10 @@ Thin local override entrypoint for admin branding and theme CSS.
25
25
  }
26
26
  } catch (error) {}
27
27
  </script>
28
+ <script src="{% static 'admin/js/theme.js' %}" defer></script>
28
29
  <script src="{% static 'admin/js/sidebar.js' %}" defer></script>
30
+ <script src="{% static 'admin/js/messages.js' %}" defer></script>
31
+ <script src="{% static 'admin/js/forms.js' %}" defer></script>
29
32
  {% endblock %}
30
33
 
31
34
  {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:'Django site admin' }}{% endblock %}
@@ -23,6 +23,7 @@
23
23
  {{ block.super }}
24
24
  {{ media.js }}
25
25
  <script src="{% static 'admin/js/filters.js' %}" defer></script>
26
+ <script src="{% static 'admin/js/changelist.js' %}" defer></script>
26
27
  {% endblock %}
27
28
 
28
29
  {% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vanta-admin
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A sharp, dark, and portable Django admin theme forged for builders who want the admin to feel less default.
5
5
  Author: oli-dev0
6
6
  License-Expression: MIT
@@ -28,7 +28,9 @@ Vanta Admin is a Django admin theme with a darker interface, tighter density,
28
28
  and a standalone structure that stays close to Django's admin template system.
29
29
 
30
30
  It is designed to be portable and to avoid frontend runtime dependencies outside
31
- standard Django admin behavior.
31
+ standard Django admin behavior. You still use Django admin; Vanta changes the
32
+ shell, navigation, common controls, messages, and visual treatment so the admin
33
+ feels more deliberate in everyday use.
32
34
 
33
35
  ## What It Adds
34
36
 
@@ -17,7 +17,11 @@ vanta_admin/static/admin/css/theme.responsive.css
17
17
  vanta_admin/static/admin/css/theme.tokens.css
18
18
  vanta_admin/static/admin/css/two_factor.css
19
19
  vanta_admin/static/admin/img/favicon.png
20
+ vanta_admin/static/admin/js/changelist.js
21
+ vanta_admin/static/admin/js/forms.js
22
+ vanta_admin/static/admin/js/messages.js
20
23
  vanta_admin/static/admin/js/sidebar.js
24
+ vanta_admin/static/admin/js/theme.js
21
25
  vanta_admin/templates/admin/app_index.html
22
26
  vanta_admin/templates/admin/base.html
23
27
  vanta_admin/templates/admin/base_site.html