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.
Files changed (72) hide show
  1. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/CHANGELOG.md +16 -0
  2. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/CONTRIBUTING.md +2 -0
  3. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/MANIFEST.in +1 -0
  4. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/PKG-INFO +7 -1
  5. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/README.md +6 -0
  6. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/__init__.py +1 -1
  7. {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
  8. {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
  9. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin.egg-info/PKG-INFO +7 -1
  10. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin.egg-info/SOURCES.txt +1 -0
  11. django_bootstrap6_admin-0.1.2/tests/js/theme_toggle.test.js +141 -0
  12. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/test_package.py +15 -0
  13. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/LICENSE +0 -0
  14. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/NOTICE +0 -0
  15. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/pyproject.toml +0 -0
  16. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/setup.cfg +0 -0
  17. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/apps.py +0 -0
  18. {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
  19. {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
  20. {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
  21. {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
  22. {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
  23. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/404.html +0 -0
  24. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/500.html +0 -0
  25. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/actions.html +0 -0
  26. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/app_index.html +0 -0
  27. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/app_list.html +0 -0
  28. {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
  29. {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
  30. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/base.html +0 -0
  31. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/base_site.html +0 -0
  32. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/change_form.html +0 -0
  33. {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
  34. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/change_list.html +0 -0
  35. {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
  36. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/change_list_results.html +0 -0
  37. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/color_theme_toggle.html +0 -0
  38. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/date_hierarchy.html +0 -0
  39. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/delete_confirmation.html +0 -0
  40. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/delete_selected_confirmation.html +0 -0
  41. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/edit_inline/stacked.html +0 -0
  42. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/edit_inline/tabular.html +0 -0
  43. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/filter.html +0 -0
  44. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/includes/fieldset.html +0 -0
  45. {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
  46. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/index.html +0 -0
  47. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/login.html +0 -0
  48. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/nav_sidebar.html +0 -0
  49. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/object_history.html +0 -0
  50. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/pagination.html +0 -0
  51. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/search_form.html +0 -0
  52. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/admin/submit_line.html +0 -0
  53. {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
  54. {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
  55. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/registration/logged_out.html +0 -0
  56. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/registration/password_change_done.html +0 -0
  57. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templates/registration/password_change_form.html +0 -0
  58. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templatetags/__init__.py +0 -0
  59. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin/templatetags/bootstrap6_admin.py +0 -0
  60. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin.egg-info/dependency_links.txt +0 -0
  61. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin.egg-info/requires.txt +0 -0
  62. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/src/django_bootstrap6_admin.egg-info/top_level.txt +0 -0
  63. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/__init__.py +0 -0
  64. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/settings.py +0 -0
  65. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/smoke_wheel.py +0 -0
  66. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/test_admin_ui.py +0 -0
  67. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/test_templatetags.py +0 -0
  68. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/testapp/__init__.py +0 -0
  69. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/testapp/admin.py +0 -0
  70. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/testapp/apps.py +0 -0
  71. {django_bootstrap6_admin-0.1.0 → django_bootstrap6_admin-0.1.2}/tests/testapp/models.py +0 -0
  72. {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
  ```
@@ -6,5 +6,6 @@ include CONTRIBUTING.md
6
6
  recursive-include src/django_bootstrap6_admin/templates *.html
7
7
  recursive-include src/django_bootstrap6_admin/static *
8
8
  recursive-include tests *.py
9
+ recursive-include tests *.js
9
10
  global-exclude __pycache__
10
11
  global-exclude *.py[cod]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-bootstrap6-admin
3
- Version: 0.1.0
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
  ```
@@ -1,3 +1,3 @@
1
1
  """Bootstrap 6 skin for the Django admin."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
@@ -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
- // First visit: start in the effective OS theme so the visible page
22
- // and the toggle icon agree immediately. Auto remains in the cycle.
23
- return media.matches ? "dark" : "light";
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
- apply(mode);
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
- apply("auto");
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.0
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
  ```
@@ -63,6 +63,7 @@ tests/test_admin_ui.py
63
63
  tests/test_package.py
64
64
  tests/test_templatetags.py
65
65
  tests/urls.py
66
+ tests/js/theme_toggle.test.js
66
67
  tests/testapp/__init__.py
67
68
  tests/testapp/admin.py
68
69
  tests/testapp/apps.py
@@ -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)