django-bootstrap6-admin 0.1.1__tar.gz → 0.2.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.
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/CHANGELOG.md +25 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/CONTRIBUTING.md +2 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/MANIFEST.in +1 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/PKG-INFO +12 -2
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/README.md +11 -1
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/__init__.py +1 -1
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/static/django_bootstrap6_admin/css/admin.css +45 -1
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/static/django_bootstrap6_admin/js/admin.js +98 -6
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/static/django_bootstrap6_admin/js/theme.js +17 -5
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin.egg-info/PKG-INFO +12 -2
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin.egg-info/SOURCES.txt +1 -0
- django_bootstrap6_admin-0.2.0/tests/js/theme_toggle.test.js +141 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/tests/test_admin_ui.py +11 -1
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/tests/test_package.py +16 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/LICENSE +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/NOTICE +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/pyproject.toml +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/setup.cfg +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/apps.py +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/static/django_bootstrap6_admin/css/widgets-overrides.css +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/static/django_bootstrap6_admin/vendor/bootstrap/LICENSE +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/static/django_bootstrap6_admin/vendor/bootstrap/css/bootstrap.min.css +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/static/django_bootstrap6_admin/vendor/bootstrap/js/bootstrap.bundle.min.js +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/404.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/500.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/actions.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/app_index.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/app_list.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/auth/user/add_form.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/auth/user/change_password.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/base.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/base_site.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/change_form.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/change_form_object_tools.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/change_list.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/change_list_object_tools.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/change_list_results.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/color_theme_toggle.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/date_hierarchy.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/delete_confirmation.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/delete_selected_confirmation.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/edit_inline/stacked.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/edit_inline/tabular.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/filter.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/includes/fieldset.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/includes/object_delete_summary.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/index.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/login.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/nav_sidebar.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/object_history.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/pagination.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/search_form.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/submit_line.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/widgets/foreign_key_raw_id.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/admin/widgets/related_widget_wrapper.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/registration/logged_out.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/registration/password_change_done.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templates/registration/password_change_form.html +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templatetags/__init__.py +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin/templatetags/bootstrap6_admin.py +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin.egg-info/dependency_links.txt +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin.egg-info/requires.txt +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/src/django_bootstrap6_admin.egg-info/top_level.txt +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/tests/__init__.py +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/tests/settings.py +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/tests/smoke_wheel.py +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/tests/test_templatetags.py +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/tests/testapp/__init__.py +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/tests/testapp/admin.py +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/tests/testapp/apps.py +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/tests/testapp/models.py +0 -0
- {django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/tests/urls.py +0 -0
|
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-11-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- The desktop filter panel can now be collapsed and expanded, and the
|
|
15
|
+
collapsed state is remembered between changelist pages.
|
|
16
|
+
- The "Show counts" / "Hide counts" choice is remembered across changelist
|
|
17
|
+
pages instead of resetting on navigation.
|
|
18
|
+
- Subtle vertical rules separate columns in changelists, inline tables and the
|
|
19
|
+
object history table.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Removed the duplicate native dropdown arrow on the bulk-action select.
|
|
24
|
+
- Improved dark-theme text contrast for secondary labels, table headers,
|
|
25
|
+
filter links, help text and links.
|
|
26
|
+
|
|
27
|
+
## [0.1.2] - 2026-11-15
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Clicking the theme toggle in a dark system no longer moves to an `auto`
|
|
32
|
+
state that resolves to dark and leaves the page unchanged. The toggle skips
|
|
33
|
+
that no-op step, so a single click always changes the visible theme.
|
|
34
|
+
|
|
10
35
|
## [0.1.1] - 2026-11-15
|
|
11
36
|
|
|
12
37
|
### 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.
|
|
3
|
+
Version: 0.2.0
|
|
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>
|
|
@@ -57,14 +57,23 @@ permissions and JavaScript remain in charge.
|
|
|
57
57
|
with a backdrop (mobile).
|
|
58
58
|
- Dashboard with application cards, model shortcuts and recent activity.
|
|
59
59
|
- Changelist with search, date hierarchy, collapsible filters, sortable
|
|
60
|
-
headers, bulk actions and Bootstrap pagination.
|
|
60
|
+
headers, bulk actions and Bootstrap pagination. The desktop filter panel can
|
|
61
|
+
be collapsed, and both its state and the "Show counts" choice are remembered
|
|
62
|
+
between pages.
|
|
63
|
+
- Subtle vertical rules separate columns in changelists, inline tables and the
|
|
64
|
+
object history.
|
|
61
65
|
- Change form with card-based fieldsets, collapsible sections, a clear save
|
|
62
66
|
bar, tabular/stacked inlines and related-object popups.
|
|
63
67
|
- Modern login, logout and password change pages.
|
|
64
68
|
- Light, dark and system themes. The existing Django `theme` localStorage
|
|
65
69
|
preference is synchronised with Bootstrap's `data-bs-theme` attribute.
|
|
70
|
+
- Dark-theme secondary text, table headers, filter links and help text use
|
|
71
|
+
higher-contrast tokens for better readability.
|
|
66
72
|
- Soft fade-in/fade-out transition when switching themes, skipped
|
|
67
73
|
automatically for users who prefer reduced motion.
|
|
74
|
+
- Every click changes the visible theme: if `auto` would resolve to the theme
|
|
75
|
+
already on screen, the toggle skips that step instead of appearing to do
|
|
76
|
+
nothing.
|
|
68
77
|
- Accessible focus states, labels, table headers and ARIA attributes.
|
|
69
78
|
- All Django admin JavaScript hooks are preserved: `actions.js`,
|
|
70
79
|
`filters.js`, `nav_sidebar.js`, inlines, Select2/autocomplete, calendar
|
|
@@ -236,6 +245,7 @@ Run the tests and the linter:
|
|
|
236
245
|
|
|
237
246
|
```bash
|
|
238
247
|
python -m django test --settings=tests.settings
|
|
248
|
+
node tests/js/theme_toggle.test.js
|
|
239
249
|
ruff check .
|
|
240
250
|
ruff format --check .
|
|
241
251
|
```
|
|
@@ -20,14 +20,23 @@ permissions and JavaScript remain in charge.
|
|
|
20
20
|
with a backdrop (mobile).
|
|
21
21
|
- Dashboard with application cards, model shortcuts and recent activity.
|
|
22
22
|
- Changelist with search, date hierarchy, collapsible filters, sortable
|
|
23
|
-
headers, bulk actions and Bootstrap pagination.
|
|
23
|
+
headers, bulk actions and Bootstrap pagination. The desktop filter panel can
|
|
24
|
+
be collapsed, and both its state and the "Show counts" choice are remembered
|
|
25
|
+
between pages.
|
|
26
|
+
- Subtle vertical rules separate columns in changelists, inline tables and the
|
|
27
|
+
object history.
|
|
24
28
|
- Change form with card-based fieldsets, collapsible sections, a clear save
|
|
25
29
|
bar, tabular/stacked inlines and related-object popups.
|
|
26
30
|
- Modern login, logout and password change pages.
|
|
27
31
|
- Light, dark and system themes. The existing Django `theme` localStorage
|
|
28
32
|
preference is synchronised with Bootstrap's `data-bs-theme` attribute.
|
|
33
|
+
- Dark-theme secondary text, table headers, filter links and help text use
|
|
34
|
+
higher-contrast tokens for better readability.
|
|
29
35
|
- Soft fade-in/fade-out transition when switching themes, skipped
|
|
30
36
|
automatically for users who prefer reduced motion.
|
|
37
|
+
- Every click changes the visible theme: if `auto` would resolve to the theme
|
|
38
|
+
already on screen, the toggle skips that step instead of appearing to do
|
|
39
|
+
nothing.
|
|
31
40
|
- Accessible focus states, labels, table headers and ARIA attributes.
|
|
32
41
|
- All Django admin JavaScript hooks are preserved: `actions.js`,
|
|
33
42
|
`filters.js`, `nav_sidebar.js`, inlines, Select2/autocomplete, calendar
|
|
@@ -199,6 +208,7 @@ Run the tests and the linter:
|
|
|
199
208
|
|
|
200
209
|
```bash
|
|
201
210
|
python -m django test --settings=tests.settings
|
|
211
|
+
node tests/js/theme_toggle.test.js
|
|
202
212
|
ruff check .
|
|
203
213
|
ruff format --check .
|
|
204
214
|
```
|
|
@@ -111,6 +111,23 @@
|
|
|
111
111
|
2. Base
|
|
112
112
|
-------------------------------------------------------------------------- */
|
|
113
113
|
|
|
114
|
+
/*
|
|
115
|
+
* Bootstrap's dark gray-500/600 secondary text is too dim on the deep navy
|
|
116
|
+
* admin surfaces, especially at the small sizes used by filters, table
|
|
117
|
+
* headers and action help. Lift those tokens for the effective dark theme.
|
|
118
|
+
*/
|
|
119
|
+
[data-bs-theme="dark"],
|
|
120
|
+
[data-effective-theme="dark"] {
|
|
121
|
+
--bs-fg-1: #f1f5f9;
|
|
122
|
+
--bs-fg-2: #d7e0ea;
|
|
123
|
+
--bs-fg-3: #b9c7d6;
|
|
124
|
+
--bs-fg-4: #9fb1c4;
|
|
125
|
+
--ba-text-muted: #c9d4e0;
|
|
126
|
+
--bs-link-color: #8dbcf7;
|
|
127
|
+
--bs-link-hover-color: #b7d4ff;
|
|
128
|
+
--bs-border-muted: #34465a;
|
|
129
|
+
}
|
|
130
|
+
|
|
114
131
|
html {
|
|
115
132
|
min-height: 100%;
|
|
116
133
|
}
|
|
@@ -1365,7 +1382,7 @@ a.ba-timeline-object:focus {
|
|
|
1365
1382
|
}
|
|
1366
1383
|
|
|
1367
1384
|
.ba-filters-toggle-row {
|
|
1368
|
-
display:
|
|
1385
|
+
display: flex;
|
|
1369
1386
|
}
|
|
1370
1387
|
|
|
1371
1388
|
.ba-filters-toggle {
|
|
@@ -1551,6 +1568,20 @@ a.ba-date-chip:focus {
|
|
|
1551
1568
|
border-block-end: 0;
|
|
1552
1569
|
}
|
|
1553
1570
|
|
|
1571
|
+
/* Very light column rules keep dense admin tables readable without making
|
|
1572
|
+
* every cell look boxed in. */
|
|
1573
|
+
#result_list thead th:not(:last-child),
|
|
1574
|
+
#result_list tbody th:not(:last-child),
|
|
1575
|
+
#result_list tbody td:not(:last-child),
|
|
1576
|
+
.ba-inline-table thead th:not(:last-child),
|
|
1577
|
+
.ba-inline-table tbody th:not(:last-child),
|
|
1578
|
+
.ba-inline-table tbody td:not(:last-child),
|
|
1579
|
+
.ba-history-table thead th:not(:last-child),
|
|
1580
|
+
.ba-history-table tbody th:not(:last-child),
|
|
1581
|
+
.ba-history-table tbody td:not(:last-child) {
|
|
1582
|
+
border-inline-end: 1px solid var(--bs-border-muted);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1554
1585
|
#result_list tbody tr.selected > * {
|
|
1555
1586
|
background: var(--bs-primary-bg-subtle) !important;
|
|
1556
1587
|
}
|
|
@@ -1597,6 +1628,7 @@ a.ba-date-chip:focus {
|
|
|
1597
1628
|
|
|
1598
1629
|
#result_list select {
|
|
1599
1630
|
padding-inline-end: 1.6rem;
|
|
1631
|
+
appearance: none;
|
|
1600
1632
|
background-image: var(--ba-select-arrow);
|
|
1601
1633
|
background-position: right 0.45rem center;
|
|
1602
1634
|
background-repeat: no-repeat;
|
|
@@ -2177,6 +2209,7 @@ label.required::after {
|
|
|
2177
2209
|
.ba-form-group select,
|
|
2178
2210
|
.ba-actions select {
|
|
2179
2211
|
padding-inline-end: 2rem;
|
|
2212
|
+
appearance: none;
|
|
2180
2213
|
background-image: var(--ba-select-arrow);
|
|
2181
2214
|
background-position: right 0.7rem center;
|
|
2182
2215
|
background-repeat: no-repeat;
|
|
@@ -2551,6 +2584,7 @@ div.help ul {
|
|
|
2551
2584
|
|
|
2552
2585
|
.ba-inline-table select {
|
|
2553
2586
|
padding-inline-end: 1.6rem;
|
|
2587
|
+
appearance: none;
|
|
2554
2588
|
background-image: var(--ba-select-arrow);
|
|
2555
2589
|
background-position: right 0.45rem center;
|
|
2556
2590
|
background-repeat: no-repeat;
|
|
@@ -3202,6 +3236,16 @@ body.popup .ba-submit-row {
|
|
|
3202
3236
|
13. Responsive
|
|
3203
3237
|
-------------------------------------------------------------------------- */
|
|
3204
3238
|
|
|
3239
|
+
@media (min-width: 1200px) {
|
|
3240
|
+
#changelist.ba-filters-collapsed .ba-changelist-layout {
|
|
3241
|
+
grid-template-columns: minmax(0, 1fr);
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
#changelist.ba-filters-collapsed .ba-filters {
|
|
3245
|
+
display: none;
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3205
3249
|
@media (max-width: 1199.98px) {
|
|
3206
3250
|
.ba-dashboard {
|
|
3207
3251
|
grid-template-columns: minmax(0, 1fr);
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
(() => {
|
|
12
12
|
const MOBILE = window.matchMedia("(max-width: 1023.98px)");
|
|
13
|
+
const DESKTOP = window.matchMedia("(min-width: 1200px)");
|
|
13
14
|
const STORAGE_KEY = "django.admin.navSidebarIsOpen";
|
|
15
|
+
const FILTERS_KEY = "django.admin.filtersCollapsed";
|
|
16
|
+
const FACETS_KEY = "django.admin.showFacets";
|
|
14
17
|
|
|
15
18
|
function initSidebar() {
|
|
16
19
|
const main = document.getElementById("main");
|
|
@@ -228,16 +231,50 @@
|
|
|
228
231
|
return;
|
|
229
232
|
}
|
|
230
233
|
|
|
234
|
+
const isCollapsed = () =>
|
|
235
|
+
changelist.classList.contains("ba-filters-collapsed");
|
|
236
|
+
|
|
231
237
|
const sync = () => {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
changelist.classList.contains("ba-filters-open")
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
+
const expanded = DESKTOP.matches
|
|
239
|
+
? !isCollapsed()
|
|
240
|
+
: changelist.classList.contains("ba-filters-open");
|
|
241
|
+
toggle.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
242
|
+
if (panel) {
|
|
243
|
+
panel.toggleAttribute(
|
|
244
|
+
"inert",
|
|
245
|
+
DESKTOP.matches && isCollapsed(),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
238
248
|
};
|
|
239
249
|
|
|
250
|
+
const setCollapsed = (collapsed) => {
|
|
251
|
+
changelist.classList.toggle("ba-filters-collapsed", collapsed);
|
|
252
|
+
if (DESKTOP.matches) {
|
|
253
|
+
localStorage.setItem(
|
|
254
|
+
FILTERS_KEY,
|
|
255
|
+
collapsed ? "true" : "false",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
sync();
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Restore the desktop preference. On mobile the panel is a drawer and
|
|
262
|
+
// the collapsed class has no visual effect.
|
|
263
|
+
if (DESKTOP.matches && localStorage.getItem(FILTERS_KEY) === "true") {
|
|
264
|
+
changelist.classList.add("ba-filters-collapsed");
|
|
265
|
+
}
|
|
266
|
+
|
|
240
267
|
toggle.addEventListener("click", () => {
|
|
268
|
+
if (DESKTOP.matches) {
|
|
269
|
+
const collapsed = !isCollapsed();
|
|
270
|
+
setCollapsed(collapsed);
|
|
271
|
+
if (!collapsed) {
|
|
272
|
+
const target = panel?.querySelector("summary, a, button");
|
|
273
|
+
window.requestAnimationFrame(() => target?.focus());
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
241
278
|
changelist.classList.toggle("ba-filters-open");
|
|
242
279
|
sync();
|
|
243
280
|
if (changelist.classList.contains("ba-filters-open")) {
|
|
@@ -249,15 +286,69 @@
|
|
|
249
286
|
|
|
250
287
|
if (close) {
|
|
251
288
|
close.addEventListener("click", () => {
|
|
289
|
+
setCollapsed(true);
|
|
252
290
|
changelist.classList.remove("ba-filters-open");
|
|
253
291
|
sync();
|
|
254
292
|
toggle.focus();
|
|
255
293
|
});
|
|
256
294
|
}
|
|
257
295
|
|
|
296
|
+
if (DESKTOP.addEventListener) {
|
|
297
|
+
DESKTOP.addEventListener("change", () => {
|
|
298
|
+
if (DESKTOP.matches) {
|
|
299
|
+
changelist.classList.toggle(
|
|
300
|
+
"ba-filters-collapsed",
|
|
301
|
+
localStorage.getItem(FILTERS_KEY) === "true",
|
|
302
|
+
);
|
|
303
|
+
changelist.classList.remove("ba-filters-open");
|
|
304
|
+
}
|
|
305
|
+
sync();
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
258
309
|
sync();
|
|
259
310
|
}
|
|
260
311
|
|
|
312
|
+
function initFacetPreference() {
|
|
313
|
+
const panel = document.getElementById("changelist-filter");
|
|
314
|
+
if (!panel) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const links = document.querySelectorAll(
|
|
319
|
+
".ba-filters-actions .hidelink, .ba-filters-actions .viewlink",
|
|
320
|
+
);
|
|
321
|
+
if (!links.length) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
links.forEach((link) => {
|
|
326
|
+
link.addEventListener("click", () => {
|
|
327
|
+
const show = link.classList.contains("viewlink");
|
|
328
|
+
localStorage.setItem(FACETS_KEY, show ? "true" : "false");
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const stored = localStorage.getItem(FACETS_KEY);
|
|
333
|
+
if (stored !== "true" && stored !== "false") {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const url = new URL(window.location.href);
|
|
338
|
+
const hasFacets = url.searchParams.has("_facets");
|
|
339
|
+
const showFacets = stored === "true";
|
|
340
|
+
if (hasFacets === showFacets) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (showFacets) {
|
|
345
|
+
url.searchParams.set("_facets", "1");
|
|
346
|
+
} else {
|
|
347
|
+
url.searchParams.delete("_facets");
|
|
348
|
+
}
|
|
349
|
+
window.location.replace(url.toString());
|
|
350
|
+
}
|
|
351
|
+
|
|
261
352
|
function initDirtyGuard() {
|
|
262
353
|
const form =
|
|
263
354
|
document.querySelector(".ba-change-form form") ||
|
|
@@ -327,6 +418,7 @@
|
|
|
327
418
|
function initAll() {
|
|
328
419
|
initSidebar();
|
|
329
420
|
initFilterDisclosure();
|
|
421
|
+
initFacetPreference();
|
|
330
422
|
initDirtyGuard();
|
|
331
423
|
initErrorFocus();
|
|
332
424
|
initAccessibility();
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
if (modes.includes(stored)) {
|
|
25
25
|
return stored;
|
|
26
26
|
}
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
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";
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
function updateButtons(mode) {
|
|
@@ -43,11 +44,15 @@
|
|
|
43
44
|
});
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
function effectiveMode(mode) {
|
|
48
|
+
mode = normalize(mode);
|
|
49
|
+
return mode === "auto" ? (media.matches ? "dark" : "light") : mode;
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
function apply(mode) {
|
|
47
53
|
mode = normalize(mode);
|
|
48
54
|
const root = document.documentElement;
|
|
49
|
-
const effective =
|
|
50
|
-
mode === "auto" ? (media.matches ? "dark" : "light") : mode;
|
|
55
|
+
const effective = effectiveMode(mode);
|
|
51
56
|
|
|
52
57
|
root.dataset.theme = mode;
|
|
53
58
|
root.dataset.effectiveTheme = effective;
|
|
@@ -121,6 +126,7 @@
|
|
|
121
126
|
|
|
122
127
|
function cycle() {
|
|
123
128
|
const mode = current();
|
|
129
|
+
const effective = effectiveMode(mode);
|
|
124
130
|
let next;
|
|
125
131
|
|
|
126
132
|
if (media.matches) {
|
|
@@ -131,6 +137,12 @@
|
|
|
131
137
|
mode === "auto" ? "dark" : mode === "dark" ? "light" : "auto";
|
|
132
138
|
}
|
|
133
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
|
+
|
|
134
146
|
set(next);
|
|
135
147
|
}
|
|
136
148
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-bootstrap6-admin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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>
|
|
@@ -57,14 +57,23 @@ permissions and JavaScript remain in charge.
|
|
|
57
57
|
with a backdrop (mobile).
|
|
58
58
|
- Dashboard with application cards, model shortcuts and recent activity.
|
|
59
59
|
- Changelist with search, date hierarchy, collapsible filters, sortable
|
|
60
|
-
headers, bulk actions and Bootstrap pagination.
|
|
60
|
+
headers, bulk actions and Bootstrap pagination. The desktop filter panel can
|
|
61
|
+
be collapsed, and both its state and the "Show counts" choice are remembered
|
|
62
|
+
between pages.
|
|
63
|
+
- Subtle vertical rules separate columns in changelists, inline tables and the
|
|
64
|
+
object history.
|
|
61
65
|
- Change form with card-based fieldsets, collapsible sections, a clear save
|
|
62
66
|
bar, tabular/stacked inlines and related-object popups.
|
|
63
67
|
- Modern login, logout and password change pages.
|
|
64
68
|
- Light, dark and system themes. The existing Django `theme` localStorage
|
|
65
69
|
preference is synchronised with Bootstrap's `data-bs-theme` attribute.
|
|
70
|
+
- Dark-theme secondary text, table headers, filter links and help text use
|
|
71
|
+
higher-contrast tokens for better readability.
|
|
66
72
|
- Soft fade-in/fade-out transition when switching themes, skipped
|
|
67
73
|
automatically for users who prefer reduced motion.
|
|
74
|
+
- Every click changes the visible theme: if `auto` would resolve to the theme
|
|
75
|
+
already on screen, the toggle skips that step instead of appearing to do
|
|
76
|
+
nothing.
|
|
68
77
|
- Accessible focus states, labels, table headers and ARIA attributes.
|
|
69
78
|
- All Django admin JavaScript hooks are preserved: `actions.js`,
|
|
70
79
|
`filters.js`, `nav_sidebar.js`, inlines, Select2/autocomplete, calendar
|
|
@@ -236,6 +245,7 @@ Run the tests and the linter:
|
|
|
236
245
|
|
|
237
246
|
```bash
|
|
238
247
|
python -m django test --settings=tests.settings
|
|
248
|
+
node tests/js/theme_toggle.test.js
|
|
239
249
|
ruff check .
|
|
240
250
|
ruff format --check .
|
|
241
251
|
```
|
|
@@ -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
|
+
})();
|
|
@@ -76,6 +76,8 @@ class AdminUiTests(TestCase):
|
|
|
76
76
|
'id="changelist-search"',
|
|
77
77
|
'id="searchbar"',
|
|
78
78
|
'id="changelist-filter"',
|
|
79
|
+
'id="ba-filters-toggle"',
|
|
80
|
+
'id="ba-filters-close"',
|
|
79
81
|
'id="action-toggle"',
|
|
80
82
|
'id="date-hierarchy-label"',
|
|
81
83
|
'class="actions ba-actions"',
|
|
@@ -119,7 +121,6 @@ class AdminUiTests(TestCase):
|
|
|
119
121
|
def test_changelist_search_and_pagination(self):
|
|
120
122
|
response = self.client.get(reverse("admin:testapp_book_changelist"), {"q": "Django"})
|
|
121
123
|
self.assertContains(response, "Django for Everyone")
|
|
122
|
-
|
|
123
124
|
for index in range(105):
|
|
124
125
|
Book.objects.create(
|
|
125
126
|
title=f"Extra book {index:03d}",
|
|
@@ -134,6 +135,15 @@ class AdminUiTests(TestCase):
|
|
|
134
135
|
self.assertContains(response, "?p=1")
|
|
135
136
|
self.assertContains(response, 'aria-current="page">2')
|
|
136
137
|
|
|
138
|
+
def test_facet_count_toggle_links(self):
|
|
139
|
+
url = reverse("admin:testapp_book_changelist")
|
|
140
|
+
|
|
141
|
+
response = self.client.get(url)
|
|
142
|
+
self.assertContains(response, "Show counts")
|
|
143
|
+
|
|
144
|
+
response = self.client.get(url, {"_facets": "1"})
|
|
145
|
+
self.assertContains(response, "Hide counts")
|
|
146
|
+
|
|
137
147
|
def test_change_form_keeps_widgets_and_submit_contract(self):
|
|
138
148
|
response = self.client.get(reverse("admin:testapp_book_change", args=[self.book.pk]))
|
|
139
149
|
|
|
@@ -46,3 +46,19 @@ class PackageAssetsTests(SimpleTestCase):
|
|
|
46
46
|
self.assertIn(".ba-theme-fade", admin_css)
|
|
47
47
|
self.assertIn(".ba-theme-fade.is-active", admin_css)
|
|
48
48
|
self.assertIn("transition: opacity 200ms ease", admin_css)
|
|
49
|
+
|
|
50
|
+
def test_changelist_ux_assets(self):
|
|
51
|
+
static_dir = Path(django_bootstrap6_admin.__file__).parent / "static"
|
|
52
|
+
admin_js = (static_dir / "django_bootstrap6_admin" / "js" / "admin.js").read_text(
|
|
53
|
+
encoding="utf-8"
|
|
54
|
+
)
|
|
55
|
+
admin_css = (static_dir / "django_bootstrap6_admin" / "css" / "admin.css").read_text(
|
|
56
|
+
encoding="utf-8"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
self.assertIn("django.admin.showFacets", admin_js)
|
|
60
|
+
self.assertIn("django.admin.filtersCollapsed", admin_js)
|
|
61
|
+
self.assertIn("ba-filters-collapsed", admin_css)
|
|
62
|
+
self.assertIn("border-inline-end: 1px solid var(--bs-border-muted)", admin_css)
|
|
63
|
+
self.assertIn("appearance: none", admin_css)
|
|
64
|
+
self.assertIn("--bs-fg-4: #9fb1c4", admin_css)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_bootstrap6_admin-0.1.1 → django_bootstrap6_admin-0.2.0}/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
|