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.
- {vanta_admin-0.2.0/vanta_admin.egg-info → vanta_admin-0.3.0}/PKG-INFO +4 -2
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/README.md +3 -1
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/pyproject.toml +1 -1
- vanta_admin-0.3.0/vanta_admin/static/admin/js/changelist.js +120 -0
- vanta_admin-0.3.0/vanta_admin/static/admin/js/forms.js +189 -0
- vanta_admin-0.3.0/vanta_admin/static/admin/js/messages.js +73 -0
- vanta_admin-0.3.0/vanta_admin/static/admin/js/sidebar.js +167 -0
- vanta_admin-0.3.0/vanta_admin/static/admin/js/theme.js +37 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/base_site.html +3 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/change_list.html +1 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0/vanta_admin.egg-info}/PKG-INFO +4 -2
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin.egg-info/SOURCES.txt +4 -0
- vanta_admin-0.2.0/vanta_admin/static/admin/js/sidebar.js +0 -563
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/LICENSE +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/MANIFEST.in +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/setup.cfg +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/__init__.py +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/apps.py +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/css/theme.admin.css +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/css/theme.base.css +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/css/theme.css +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/css/theme.login.css +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/css/theme.responsive.css +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/css/theme.tokens.css +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/css/two_factor.css +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/static/admin/img/favicon.png +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/app_index.html +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/base.html +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/change_form.html +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/filter.html +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/includes/breadcrumbs.html +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/index.html +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/login.html +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/admin/nav_sidebar.html +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/two_factor/_base.html +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/two_factor/_base_focus.html +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/two_factor/_wizard_actions.html +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/two_factor/core/login.html +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/two_factor/core/setup_complete.html +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templates/two_factor/profile/profile.html +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templatetags/__init__.py +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin/templatetags/admin_theme.py +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin.egg-info/dependency_links.txt +0 -0
- {vanta_admin-0.2.0 → vanta_admin-0.3.0}/vanta_admin.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|