django-bootstrap6-admin 0.1.0__tar.gz → 0.1.2__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.
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/CHANGELOG.md +16 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/CONTRIBUTING.md +2 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/MANIFEST.in +1 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/PKG-INFO +7 -1
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/README.md +6 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/__init__.py +1 -1
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/static/django_bootstrap6_admin/css/admin.css +25 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/static/django_bootstrap6_admin/js/theme.js +78 -7
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin.egg-info/PKG-INFO +7 -1
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin.egg-info/SOURCES.txt +1 -0
- django_bootstrap6_admin-0.1.2/tests/js/theme_toggle.test.js +141 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/test_package.py +15 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/LICENSE +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/NOTICE +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/pyproject.toml +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/setup.cfg +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/apps.py +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/static/django_bootstrap6_admin/css/widgets-overrides.css +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/static/django_bootstrap6_admin/js/admin.js +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/static/django_bootstrap6_admin/vendor/bootstrap/LICENSE +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/static/django_bootstrap6_admin/vendor/bootstrap/css/bootstrap.min.css +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/static/django_bootstrap6_admin/vendor/bootstrap/js/bootstrap.bundle.min.js +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/404.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/500.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/actions.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/app_index.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/app_list.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/auth/user/add_form.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/auth/user/change_password.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/base.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/base_site.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/change_form.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/change_form_object_tools.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/change_list.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/change_list_object_tools.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/change_list_results.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/color_theme_toggle.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/date_hierarchy.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/delete_confirmation.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/delete_selected_confirmation.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/edit_inline/stacked.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/edit_inline/tabular.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/filter.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/includes/fieldset.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/includes/object_delete_summary.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/index.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/login.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/nav_sidebar.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/object_history.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/pagination.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/search_form.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/submit_line.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/widgets/foreign_key_raw_id.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/widgets/related_widget_wrapper.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/registration/logged_out.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/registration/password_change_done.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/registration/password_change_form.html +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templatetags/__init__.py +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templatetags/bootstrap6_admin.py +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin.egg-info/dependency_links.txt +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin.egg-info/requires.txt +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin.egg-info/top_level.txt +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/__init__.py +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/settings.py +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/smoke_wheel.py +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/test_admin_ui.py +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/test_templatetags.py +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/testapp/__init__.py +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/testapp/admin.py +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/testapp/apps.py +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/testapp/models.py +0 -0
- {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/urls.py +0 -0
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.2] - 2026-11-15
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Clicking the theme toggle in a dark system no longer moves to an `auto`
|
|
15
|
+
state that resolves to dark and leaves the page unchanged. The toggle skips
|
|
16
|
+
that no-op step, so a single click always changes the visible theme.
|
|
17
|
+
|
|
18
|
+
## [0.1.1] - 2026-11-15
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Soft fade-in/fade-out transition when switching between light, dark and auto
|
|
23
|
+
themes. The animation is skipped when `prefers-reduced-motion: reduce` is
|
|
24
|
+
set.
|
|
25
|
+
|
|
10
26
|
## [0.1.0] - 2026-11-15
|
|
11
27
|
|
|
12
28
|
### Added
|
|
@@ -23,6 +23,8 @@ python -m django test --settings=tests.settings
|
|
|
23
23
|
Run the linter and formatter check before opening a pull request:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
+
python -m django test --settings=tests.settings
|
|
27
|
+
node tests/js/theme_toggle.test.js
|
|
26
28
|
ruff check .
|
|
27
29
|
ruff format --check .
|
|
28
30
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-bootstrap6-admin
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: A modern Bootstrap 6 skin for the Django admin.
|
|
5
5
|
Author-email: Aliberk Sandıkçı <git@asandikci.com>
|
|
6
6
|
Maintainer-email: Aliberk Sandıkçı <git@asandikci.com>
|
|
@@ -63,6 +63,11 @@ permissions and JavaScript remain in charge.
|
|
|
63
63
|
- Modern login, logout and password change pages.
|
|
64
64
|
- Light, dark and system themes. The existing Django `theme` localStorage
|
|
65
65
|
preference is synchronised with Bootstrap's `data-bs-theme` attribute.
|
|
66
|
+
- Soft fade-in/fade-out transition when switching themes, skipped
|
|
67
|
+
automatically for users who prefer reduced motion.
|
|
68
|
+
- Every click changes the visible theme: if `auto` would resolve to the theme
|
|
69
|
+
already on screen, the toggle skips that step instead of appearing to do
|
|
70
|
+
nothing.
|
|
66
71
|
- Accessible focus states, labels, table headers and ARIA attributes.
|
|
67
72
|
- All Django admin JavaScript hooks are preserved: `actions.js`,
|
|
68
73
|
`filters.js`, `nav_sidebar.js`, inlines, Select2/autocomplete, calendar
|
|
@@ -234,6 +239,7 @@ Run the tests and the linter:
|
|
|
234
239
|
|
|
235
240
|
```bash
|
|
236
241
|
python -m django test --settings=tests.settings
|
|
242
|
+
node tests/js/theme_toggle.test.js
|
|
237
243
|
ruff check .
|
|
238
244
|
ruff format --check .
|
|
239
245
|
```
|
|
@@ -26,6 +26,11 @@ permissions and JavaScript remain in charge.
|
|
|
26
26
|
- Modern login, logout and password change pages.
|
|
27
27
|
- Light, dark and system themes. The existing Django `theme` localStorage
|
|
28
28
|
preference is synchronised with Bootstrap's `data-bs-theme` attribute.
|
|
29
|
+
- Soft fade-in/fade-out transition when switching themes, skipped
|
|
30
|
+
automatically for users who prefer reduced motion.
|
|
31
|
+
- Every click changes the visible theme: if `auto` would resolve to the theme
|
|
32
|
+
already on screen, the toggle skips that step instead of appearing to do
|
|
33
|
+
nothing.
|
|
29
34
|
- Accessible focus states, labels, table headers and ARIA attributes.
|
|
30
35
|
- All Django admin JavaScript hooks are preserved: `actions.js`,
|
|
31
36
|
`filters.js`, `nav_sidebar.js`, inlines, Select2/autocomplete, calendar
|
|
@@ -197,6 +202,7 @@ Run the tests and the linter:
|
|
|
197
202
|
|
|
198
203
|
```bash
|
|
199
204
|
python -m django test --settings=tests.settings
|
|
205
|
+
node tests/js/theme_toggle.test.js
|
|
200
206
|
ruff check .
|
|
201
207
|
ruff format --check .
|
|
202
208
|
```
|
|
@@ -127,6 +127,31 @@ body.ba-body {
|
|
|
127
127
|
line-height: 1.55;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/*
|
|
131
|
+
* Full-screen veil used by theme.js. The script fades it in over the current
|
|
132
|
+
* canvas colour, applies the new theme while the veil is opaque, then fades
|
|
133
|
+
* it out to reveal the new colours.
|
|
134
|
+
*/
|
|
135
|
+
.ba-theme-fade {
|
|
136
|
+
position: fixed;
|
|
137
|
+
inset: 0;
|
|
138
|
+
z-index: 3000;
|
|
139
|
+
background: var(--ba-canvas);
|
|
140
|
+
opacity: 0;
|
|
141
|
+
pointer-events: none;
|
|
142
|
+
transition: opacity 200ms ease;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.ba-theme-fade.is-active {
|
|
146
|
+
opacity: 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@media (prefers-reduced-motion: reduce) {
|
|
150
|
+
.ba-theme-fade {
|
|
151
|
+
transition: none;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
130
155
|
#container.ba-app {
|
|
131
156
|
display: flex;
|
|
132
157
|
flex: 1 0 auto;
|
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
(() => {
|
|
8
8
|
const STORAGE_KEY = "theme";
|
|
9
9
|
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
10
|
+
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
10
11
|
const modes = ["auto", "light", "dark"];
|
|
12
|
+
const FADE_MS = 220; // Slightly longer than the 200ms CSS transition.
|
|
13
|
+
|
|
14
|
+
let overlay;
|
|
15
|
+
let fading = false;
|
|
16
|
+
let pendingMode = null;
|
|
11
17
|
|
|
12
18
|
function normalize(mode) {
|
|
13
19
|
return modes.includes(mode) ? mode : "auto";
|
|
@@ -18,9 +24,10 @@
|
|
|
18
24
|
if (modes.includes(stored)) {
|
|
19
25
|
return stored;
|
|
20
26
|
}
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
27
|
+
// No explicit preference yet: follow the system. Keeping the mode as
|
|
28
|
+
// "auto" prevents the first click from landing on an auto state that
|
|
29
|
+
// resolves to the same theme.
|
|
30
|
+
return "auto";
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
function updateButtons(mode) {
|
|
@@ -37,11 +44,15 @@
|
|
|
37
44
|
});
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
function effectiveMode(mode) {
|
|
48
|
+
mode = normalize(mode);
|
|
49
|
+
return mode === "auto" ? (media.matches ? "dark" : "light") : mode;
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
function apply(mode) {
|
|
41
53
|
mode = normalize(mode);
|
|
42
54
|
const root = document.documentElement;
|
|
43
|
-
const effective =
|
|
44
|
-
mode === "auto" ? (media.matches ? "dark" : "light") : mode;
|
|
55
|
+
const effective = effectiveMode(mode);
|
|
45
56
|
|
|
46
57
|
root.dataset.theme = mode;
|
|
47
58
|
root.dataset.effectiveTheme = effective;
|
|
@@ -54,14 +65,68 @@
|
|
|
54
65
|
updateButtons(mode);
|
|
55
66
|
}
|
|
56
67
|
|
|
68
|
+
function canvasColor() {
|
|
69
|
+
const bodyStyles = getComputedStyle(document.body);
|
|
70
|
+
if (bodyStyles.backgroundColor !== "rgba(0, 0, 0, 0)") {
|
|
71
|
+
return bodyStyles.backgroundColor;
|
|
72
|
+
}
|
|
73
|
+
return getComputedStyle(document.documentElement).backgroundColor;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function overlayElement() {
|
|
77
|
+
if (overlay) {
|
|
78
|
+
return overlay;
|
|
79
|
+
}
|
|
80
|
+
overlay = document.createElement("div");
|
|
81
|
+
overlay.className = "ba-theme-fade";
|
|
82
|
+
overlay.setAttribute("aria-hidden", "true");
|
|
83
|
+
document.body.appendChild(overlay);
|
|
84
|
+
return overlay;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function transitionTo(mode) {
|
|
88
|
+
mode = normalize(mode);
|
|
89
|
+
|
|
90
|
+
if (reducedMotion.matches || !document.body) {
|
|
91
|
+
apply(mode);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (fading) {
|
|
96
|
+
pendingMode = mode;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const element = overlayElement();
|
|
101
|
+
element.style.backgroundColor = canvasColor();
|
|
102
|
+
// Commit the transparent state so the following class change animates.
|
|
103
|
+
void element.offsetWidth;
|
|
104
|
+
element.classList.add("is-active");
|
|
105
|
+
fading = true;
|
|
106
|
+
|
|
107
|
+
window.setTimeout(() => {
|
|
108
|
+
apply(mode);
|
|
109
|
+
element.classList.remove("is-active");
|
|
110
|
+
window.setTimeout(() => {
|
|
111
|
+
fading = false;
|
|
112
|
+
const next = pendingMode;
|
|
113
|
+
pendingMode = null;
|
|
114
|
+
if (next && next !== mode) {
|
|
115
|
+
transitionTo(next);
|
|
116
|
+
}
|
|
117
|
+
}, FADE_MS);
|
|
118
|
+
}, FADE_MS);
|
|
119
|
+
}
|
|
120
|
+
|
|
57
121
|
function set(mode) {
|
|
58
122
|
mode = normalize(mode);
|
|
59
123
|
localStorage.setItem(STORAGE_KEY, mode);
|
|
60
|
-
|
|
124
|
+
transitionTo(mode);
|
|
61
125
|
}
|
|
62
126
|
|
|
63
127
|
function cycle() {
|
|
64
128
|
const mode = current();
|
|
129
|
+
const effective = effectiveMode(mode);
|
|
65
130
|
let next;
|
|
66
131
|
|
|
67
132
|
if (media.matches) {
|
|
@@ -72,6 +137,12 @@
|
|
|
72
137
|
mode === "auto" ? "dark" : mode === "dark" ? "light" : "auto";
|
|
73
138
|
}
|
|
74
139
|
|
|
140
|
+
// In dark systems auto is dark, and in light systems auto is light.
|
|
141
|
+
// Skip that no-op step so every click visibly changes the theme.
|
|
142
|
+
if (effectiveMode(next) === effective) {
|
|
143
|
+
next = effective === "dark" ? "light" : "dark";
|
|
144
|
+
}
|
|
145
|
+
|
|
75
146
|
set(next);
|
|
76
147
|
}
|
|
77
148
|
|
|
@@ -99,7 +170,7 @@
|
|
|
99
170
|
if (media.addEventListener) {
|
|
100
171
|
media.addEventListener("change", () => {
|
|
101
172
|
if (current() === "auto") {
|
|
102
|
-
|
|
173
|
+
transitionTo("auto");
|
|
103
174
|
}
|
|
104
175
|
});
|
|
105
176
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-bootstrap6-admin
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: A modern Bootstrap 6 skin for the Django admin.
|
|
5
5
|
Author-email: Aliberk Sandıkçı <git@asandikci.com>
|
|
6
6
|
Maintainer-email: Aliberk Sandıkçı <git@asandikci.com>
|
|
@@ -63,6 +63,11 @@ permissions and JavaScript remain in charge.
|
|
|
63
63
|
- Modern login, logout and password change pages.
|
|
64
64
|
- Light, dark and system themes. The existing Django `theme` localStorage
|
|
65
65
|
preference is synchronised with Bootstrap's `data-bs-theme` attribute.
|
|
66
|
+
- Soft fade-in/fade-out transition when switching themes, skipped
|
|
67
|
+
automatically for users who prefer reduced motion.
|
|
68
|
+
- Every click changes the visible theme: if `auto` would resolve to the theme
|
|
69
|
+
already on screen, the toggle skips that step instead of appearing to do
|
|
70
|
+
nothing.
|
|
66
71
|
- Accessible focus states, labels, table headers and ARIA attributes.
|
|
67
72
|
- All Django admin JavaScript hooks are preserved: `actions.js`,
|
|
68
73
|
`filters.js`, `nav_sidebar.js`, inlines, Select2/autocomplete, calendar
|
|
@@ -234,6 +239,7 @@ Run the tests and the linter:
|
|
|
234
239
|
|
|
235
240
|
```bash
|
|
236
241
|
python -m django test --settings=tests.settings
|
|
242
|
+
node tests/js/theme_toggle.test.js
|
|
237
243
|
ruff check .
|
|
238
244
|
ruff format --check .
|
|
239
245
|
```
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const vm = require("vm");
|
|
3
|
+
|
|
4
|
+
const source = fs.readFileSync(
|
|
5
|
+
"src/django_bootstrap6_admin/static/django_bootstrap6_admin/js/theme.js",
|
|
6
|
+
"utf8",
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
class ClassList {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.values = new Set();
|
|
12
|
+
}
|
|
13
|
+
add(value) {
|
|
14
|
+
this.values.add(value);
|
|
15
|
+
}
|
|
16
|
+
remove(value) {
|
|
17
|
+
this.values.delete(value);
|
|
18
|
+
}
|
|
19
|
+
contains(value) {
|
|
20
|
+
return this.values.has(value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeElement() {
|
|
25
|
+
return {
|
|
26
|
+
style: {},
|
|
27
|
+
classList: new ClassList(),
|
|
28
|
+
dataset: {},
|
|
29
|
+
attrs: {},
|
|
30
|
+
offsetWidth: 0,
|
|
31
|
+
setAttribute(name, value) {
|
|
32
|
+
this.attrs[name] = value;
|
|
33
|
+
},
|
|
34
|
+
appendChild() {},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sleep(ms) {
|
|
39
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function effective(mode, prefersDark) {
|
|
43
|
+
return mode === "auto" ? (prefersDark ? "dark" : "light") : mode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function run({ prefersDark, stored, reducedMotion = false }) {
|
|
47
|
+
const root = makeElement();
|
|
48
|
+
const body = makeElement();
|
|
49
|
+
const localStorage = {
|
|
50
|
+
value: stored,
|
|
51
|
+
getItem() {
|
|
52
|
+
return this.value;
|
|
53
|
+
},
|
|
54
|
+
setItem(_key, value) {
|
|
55
|
+
this.value = value;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const sandbox = {
|
|
59
|
+
console,
|
|
60
|
+
setTimeout,
|
|
61
|
+
clearTimeout,
|
|
62
|
+
localStorage,
|
|
63
|
+
getComputedStyle() {
|
|
64
|
+
return { backgroundColor: "rgb(13, 20, 28)" };
|
|
65
|
+
},
|
|
66
|
+
document: {
|
|
67
|
+
documentElement: root,
|
|
68
|
+
body,
|
|
69
|
+
readyState: "complete",
|
|
70
|
+
createElement: makeElement,
|
|
71
|
+
querySelectorAll() {
|
|
72
|
+
return [];
|
|
73
|
+
},
|
|
74
|
+
addEventListener() {},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
sandbox.window = sandbox;
|
|
78
|
+
sandbox.window.matchMedia = (query) => ({
|
|
79
|
+
matches: query.includes("prefers-reduced-motion")
|
|
80
|
+
? reducedMotion
|
|
81
|
+
: query.includes("prefers-color-scheme") && prefersDark,
|
|
82
|
+
addEventListener() {},
|
|
83
|
+
});
|
|
84
|
+
sandbox.window.setTimeout = setTimeout;
|
|
85
|
+
|
|
86
|
+
vm.runInNewContext(source, sandbox, { filename: "theme.js" });
|
|
87
|
+
|
|
88
|
+
const api = sandbox.window.Bootstrap6AdminTheme;
|
|
89
|
+
const before = root.dataset.theme;
|
|
90
|
+
const beforeEffective = root.dataset.effectiveTheme;
|
|
91
|
+
api.cycle();
|
|
92
|
+
await sleep(reducedMotion ? 10 : 600);
|
|
93
|
+
const after = root.dataset.theme;
|
|
94
|
+
const afterEffective = root.dataset.effectiveTheme;
|
|
95
|
+
|
|
96
|
+
return { before, beforeEffective, after, afterEffective };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
(async () => {
|
|
100
|
+
const cases = [
|
|
101
|
+
{ label: "dark system, no stored theme", prefersDark: true, stored: null },
|
|
102
|
+
{ label: "dark system, stored auto", prefersDark: true, stored: "auto" },
|
|
103
|
+
{ label: "dark system, stored dark", prefersDark: true, stored: "dark" },
|
|
104
|
+
{ label: "dark system, stored light", prefersDark: true, stored: "light" },
|
|
105
|
+
{ label: "light system, no stored theme", prefersDark: false, stored: null },
|
|
106
|
+
{ label: "light system, stored auto", prefersDark: false, stored: "auto" },
|
|
107
|
+
{ label: "light system, stored light", prefersDark: false, stored: "light" },
|
|
108
|
+
{ label: "light system, stored dark", prefersDark: false, stored: "dark" },
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
let failed = false;
|
|
112
|
+
for (const testCase of cases) {
|
|
113
|
+
const result = await run(testCase);
|
|
114
|
+
const changed = result.beforeEffective !== result.afterEffective;
|
|
115
|
+
if (!changed) {
|
|
116
|
+
failed = true;
|
|
117
|
+
}
|
|
118
|
+
console.log(
|
|
119
|
+
`${changed ? "PASS" : "FAIL"} ${testCase.label}: ` +
|
|
120
|
+
`${result.before}/${result.beforeEffective} -> ` +
|
|
121
|
+
`${result.after}/${result.afterEffective}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const reduced = await run({
|
|
126
|
+
prefersDark: true,
|
|
127
|
+
stored: "dark",
|
|
128
|
+
reducedMotion: true,
|
|
129
|
+
});
|
|
130
|
+
const reducedChanged = reduced.beforeEffective !== reduced.afterEffective;
|
|
131
|
+
if (!reducedChanged) {
|
|
132
|
+
failed = true;
|
|
133
|
+
}
|
|
134
|
+
console.log(
|
|
135
|
+
`${reducedChanged ? "PASS" : "FAIL"} reduced motion: ` +
|
|
136
|
+
`${reduced.before}/${reduced.beforeEffective} -> ` +
|
|
137
|
+
`${reduced.after}/${reduced.afterEffective}`,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
process.exit(failed ? 1 : 0);
|
|
141
|
+
})();
|
|
@@ -31,3 +31,18 @@ class PackageAssetsTests(SimpleTestCase):
|
|
|
31
31
|
for asset in STATIC_ASSETS:
|
|
32
32
|
with self.subTest(asset=asset):
|
|
33
33
|
self.assertIsNotNone(finders.find(asset))
|
|
34
|
+
|
|
35
|
+
def test_theme_transition_assets(self):
|
|
36
|
+
static_dir = Path(django_bootstrap6_admin.__file__).parent / "static"
|
|
37
|
+
theme_js = (static_dir / "django_bootstrap6_admin" / "js" / "theme.js").read_text(
|
|
38
|
+
encoding="utf-8"
|
|
39
|
+
)
|
|
40
|
+
admin_css = (static_dir / "django_bootstrap6_admin" / "css" / "admin.css").read_text(
|
|
41
|
+
encoding="utf-8"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
self.assertIn("ba-theme-fade", theme_js)
|
|
45
|
+
self.assertIn("prefers-reduced-motion", theme_js)
|
|
46
|
+
self.assertIn(".ba-theme-fade", admin_css)
|
|
47
|
+
self.assertIn(".ba-theme-fade.is-active", admin_css)
|
|
48
|
+
self.assertIn("transition: opacity 200ms ease", admin_css)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/apps.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|