django-admin-grove 0.1.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_admin_grove-0.1.0/MANIFEST.in +3 -0
- django_admin_grove-0.1.0/PKG-INFO +91 -0
- django_admin_grove-0.1.0/README.md +78 -0
- django_admin_grove-0.1.0/django_admin_grove/__init__.py +0 -0
- django_admin_grove-0.1.0/django_admin_grove/apps.py +7 -0
- django_admin_grove-0.1.0/django_admin_grove/static/django_admin_grove/css/print.css +42 -0
- django_admin_grove-0.1.0/django_admin_grove/static/django_admin_grove/css/theme.css +262 -0
- django_admin_grove-0.1.0/django_admin_grove/templates/admin/base.html +9 -0
- django_admin_grove-0.1.0/django_admin_grove.egg-info/PKG-INFO +91 -0
- django_admin_grove-0.1.0/django_admin_grove.egg-info/SOURCES.txt +17 -0
- django_admin_grove-0.1.0/django_admin_grove.egg-info/dependency_links.txt +1 -0
- django_admin_grove-0.1.0/django_admin_grove.egg-info/requires.txt +1 -0
- django_admin_grove-0.1.0/django_admin_grove.egg-info/top_level.txt +1 -0
- django_admin_grove-0.1.0/pyproject.toml +24 -0
- django_admin_grove-0.1.0/setup.cfg +4 -0
- django_admin_grove-0.1.0/tests/__init__.py +0 -0
- django_admin_grove-0.1.0/tests/settings.py +29 -0
- django_admin_grove-0.1.0/tests/test_theme.py +32 -0
- django_admin_grove-0.1.0/tests/urls.py +4 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-admin-grove
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A forest-green theme for Django admin, with light, dark, and print styles.
|
|
5
|
+
Author: Fabian Braun
|
|
6
|
+
Classifier: Framework :: Django
|
|
7
|
+
Classifier: Framework :: Django :: 5.2
|
|
8
|
+
Classifier: Framework :: Django :: 6.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: Django<6.1,>=5.2
|
|
13
|
+
|
|
14
|
+
# Django Admin Grove
|
|
15
|
+
|
|
16
|
+
A forest-green theme for Django admin with rounded surfaces, light and dark modes,
|
|
17
|
+
responsive layouts, and a print stylesheet. No JavaScript, external fonts, models,
|
|
18
|
+
or migrations are required. Uses Django's existing theme toggle and branding.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Install directly from Codeberg:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
python -m pip install git+https://codeberg.org/fsbraun/django-admin-grove
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Add Grove **before** Django's admin app (including custom AdminConfig subclasses):
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
INSTALLED_APPS = [
|
|
32
|
+
"django_admin_grove",
|
|
33
|
+
"django.contrib.admin",
|
|
34
|
+
# ...
|
|
35
|
+
]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Use Django's app template loader (`APP_DIRS = True`, or the explicit
|
|
39
|
+
`django.template.loaders.app_directories.Loader`) and staticfiles as usual.
|
|
40
|
+
Run `python manage.py collectstatic` for production deployments.
|
|
41
|
+
No URL configuration or database changes are needed. Django CMS is optional.
|
|
42
|
+
|
|
43
|
+
Supports Django 5.2 and 6.0; use a Python version supported by your Django release.
|
|
44
|
+
|
|
45
|
+
## Customization
|
|
46
|
+
|
|
47
|
+
Set `admin.site.site_header`, `site_title`, and `index_title` normally, or override
|
|
48
|
+
`admin/base_site.html` in your project. Grove leaves Django's branding blocks intact.
|
|
49
|
+
Project templates should extend `admin/base.html` and keep `block.super` when
|
|
50
|
+
replacing the `responsive` block.
|
|
51
|
+
|
|
52
|
+
Load project CSS after Grove using its `grove_styles` block:
|
|
53
|
+
|
|
54
|
+
```django
|
|
55
|
+
{% extends "admin/base.html" %}
|
|
56
|
+
{% load static %}
|
|
57
|
+
{% block grove_styles %}
|
|
58
|
+
<link rel="stylesheet" href="{% static 'myproject/admin.css' %}">
|
|
59
|
+
{% endblock %}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Override the `--theme-*` CSS variables for colors and surfaces. Match Grove's
|
|
63
|
+
`html[data-theme="light"]` and `html[data-theme="dark"]` selectors when customizing
|
|
64
|
+
both modes. System preferences apply when Django selects automatic mode.
|
|
65
|
+
The `--dca-primary` and `--dca-secondary` variables follow the active palette.
|
|
66
|
+
|
|
67
|
+
Printing uses A4 landscape and hides admin controls. Use `.report-screen-only`
|
|
68
|
+
to hide custom controls in print and `.report-print-only` for print-only context.
|
|
69
|
+
Domain-specific selectors and branding belong in the consuming project.
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
python -m pip install -e .
|
|
75
|
+
python -m django test tests --settings=tests.settings
|
|
76
|
+
python -m pip install build
|
|
77
|
+
python -m build
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Tests run against plain Django admin with an in-memory database.
|
|
81
|
+
|
|
82
|
+
## Publishing
|
|
83
|
+
|
|
84
|
+
Enable Actions in the Codeberg repository settings and make a runner with the
|
|
85
|
+
`codeberg-local` label available. Add a PyPI API token as the Actions secret
|
|
86
|
+
`PYPI_API_TOKEN`.
|
|
87
|
+
|
|
88
|
+
Update the version in `pyproject.toml`, then publish a Codeberg release with a
|
|
89
|
+
matching tag (for example, `0.1.0` or `v0.1.0`). The publication workflow checks
|
|
90
|
+
the version, runs the tests, builds and validates the wheel and source
|
|
91
|
+
distribution, and uploads both to PyPI.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Django Admin Grove
|
|
2
|
+
|
|
3
|
+
A forest-green theme for Django admin with rounded surfaces, light and dark modes,
|
|
4
|
+
responsive layouts, and a print stylesheet. No JavaScript, external fonts, models,
|
|
5
|
+
or migrations are required. Uses Django's existing theme toggle and branding.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Install directly from Codeberg:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
python -m pip install git+https://codeberg.org/fsbraun/django-admin-grove
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Add Grove **before** Django's admin app (including custom AdminConfig subclasses):
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
INSTALLED_APPS = [
|
|
19
|
+
"django_admin_grove",
|
|
20
|
+
"django.contrib.admin",
|
|
21
|
+
# ...
|
|
22
|
+
]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Use Django's app template loader (`APP_DIRS = True`, or the explicit
|
|
26
|
+
`django.template.loaders.app_directories.Loader`) and staticfiles as usual.
|
|
27
|
+
Run `python manage.py collectstatic` for production deployments.
|
|
28
|
+
No URL configuration or database changes are needed. Django CMS is optional.
|
|
29
|
+
|
|
30
|
+
Supports Django 5.2 and 6.0; use a Python version supported by your Django release.
|
|
31
|
+
|
|
32
|
+
## Customization
|
|
33
|
+
|
|
34
|
+
Set `admin.site.site_header`, `site_title`, and `index_title` normally, or override
|
|
35
|
+
`admin/base_site.html` in your project. Grove leaves Django's branding blocks intact.
|
|
36
|
+
Project templates should extend `admin/base.html` and keep `block.super` when
|
|
37
|
+
replacing the `responsive` block.
|
|
38
|
+
|
|
39
|
+
Load project CSS after Grove using its `grove_styles` block:
|
|
40
|
+
|
|
41
|
+
```django
|
|
42
|
+
{% extends "admin/base.html" %}
|
|
43
|
+
{% load static %}
|
|
44
|
+
{% block grove_styles %}
|
|
45
|
+
<link rel="stylesheet" href="{% static 'myproject/admin.css' %}">
|
|
46
|
+
{% endblock %}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Override the `--theme-*` CSS variables for colors and surfaces. Match Grove's
|
|
50
|
+
`html[data-theme="light"]` and `html[data-theme="dark"]` selectors when customizing
|
|
51
|
+
both modes. System preferences apply when Django selects automatic mode.
|
|
52
|
+
The `--dca-primary` and `--dca-secondary` variables follow the active palette.
|
|
53
|
+
|
|
54
|
+
Printing uses A4 landscape and hides admin controls. Use `.report-screen-only`
|
|
55
|
+
to hide custom controls in print and `.report-print-only` for print-only context.
|
|
56
|
+
Domain-specific selectors and branding belong in the consuming project.
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
python -m pip install -e .
|
|
62
|
+
python -m django test tests --settings=tests.settings
|
|
63
|
+
python -m pip install build
|
|
64
|
+
python -m build
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Tests run against plain Django admin with an in-memory database.
|
|
68
|
+
|
|
69
|
+
## Publishing
|
|
70
|
+
|
|
71
|
+
Enable Actions in the Codeberg repository settings and make a runner with the
|
|
72
|
+
`codeberg-local` label available. Add a PyPI API token as the Actions secret
|
|
73
|
+
`PYPI_API_TOKEN`.
|
|
74
|
+
|
|
75
|
+
Update the version in `pyproject.toml`, then publish a Codeberg release with a
|
|
76
|
+
matching tag (for example, `0.1.0` or `v0.1.0`). The publication workflow checks
|
|
77
|
+
the version, runs the tests, builds and validates the wheel and source
|
|
78
|
+
distribution, and uploads both to PyPI.
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Print layout for the whole admin: A4 landscape, no chrome, content only. */
|
|
2
|
+
@page {
|
|
3
|
+
size: A4 landscape;
|
|
4
|
+
margin: 12mm;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/* Admin chrome and interactive controls */
|
|
8
|
+
#header, #nav-sidebar, #toggle-nav-sidebar, .breadcrumbs, #footer,
|
|
9
|
+
.messagelist, .object-tools,
|
|
10
|
+
.report-screen-only,
|
|
11
|
+
/* changelist controls */
|
|
12
|
+
#changelist-filter, #changelist-search, #changelist #toolbar, #changelist .actions,
|
|
13
|
+
.action-checkbox, .action-checkbox-column, #changelist .paginator a,
|
|
14
|
+
/* change form controls */
|
|
15
|
+
.submit-row, .related-widget-wrapper-link { display: none !important; }
|
|
16
|
+
|
|
17
|
+
html, body { background: #fff !important; color: #000 !important; }
|
|
18
|
+
#container, #main, #content, #content-main, #changelist, #changelist .results {
|
|
19
|
+
margin: 0 !important;
|
|
20
|
+
padding: 0 !important;
|
|
21
|
+
width: auto !important;
|
|
22
|
+
min-width: 0 !important;
|
|
23
|
+
max-width: none !important;
|
|
24
|
+
box-shadow: none !important;
|
|
25
|
+
border: 0 !important;
|
|
26
|
+
border-radius: 0 !important;
|
|
27
|
+
overflow: visible !important;
|
|
28
|
+
}
|
|
29
|
+
#content h1 { font-size: 16pt; margin: 0 0 4pt; }
|
|
30
|
+
.report-print-only { display: block !important; margin: 0 0 8pt; font-size: 10pt; color: #333; }
|
|
31
|
+
|
|
32
|
+
.module { border: 0 !important; border-radius: 0 !important; box-shadow: none !important; }
|
|
33
|
+
table { width: 100%; border-collapse: collapse; font-size: 9pt; }
|
|
34
|
+
thead { display: table-header-group; }
|
|
35
|
+
tr { page-break-inside: avoid; }
|
|
36
|
+
th, td { padding: 2pt 4pt !important; border-bottom: 1px solid #ccc; background: transparent !important; color: #000 !important; }
|
|
37
|
+
thead th { background: #eee !important; }
|
|
38
|
+
thead th .text a, thead th .sortoptions { color: #000 !important; }
|
|
39
|
+
a, a:link, a:visited { color: inherit !important; text-decoration: none !important; }
|
|
40
|
+
|
|
41
|
+
/* Detail blocks print as the reader left them (open or closed). */
|
|
42
|
+
details > summary { color: #000; }
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/* Grove admin: forest greens and rounded surfaces.
|
|
2
|
+
Loaded after Django's page-specific and responsive styles. */
|
|
3
|
+
:root, html[data-theme="light"], html[data-theme="dark"] {
|
|
4
|
+
--theme-bg: #ffffff;
|
|
5
|
+
--theme-surface: #f4f8f6;
|
|
6
|
+
--theme-text: #13201a;
|
|
7
|
+
--theme-muted: #5b6b63;
|
|
8
|
+
--theme-border: #dde7e2;
|
|
9
|
+
--theme-brand: #0c4b33;
|
|
10
|
+
--theme-accent: #2f9e73;
|
|
11
|
+
--theme-soft: #e3f4ec;
|
|
12
|
+
--dca-primary: var(--theme-brand);
|
|
13
|
+
--dca-secondary: var(--theme-soft);
|
|
14
|
+
--theme-on-brand: #ffffff;
|
|
15
|
+
--theme-warning: #b45309;
|
|
16
|
+
--theme-warning-bg: #fdf1e3;
|
|
17
|
+
--theme-error: #a52a36;
|
|
18
|
+
--theme-error-bg: #fff0f1;
|
|
19
|
+
--theme-radius: 14px;
|
|
20
|
+
--primary: var(--theme-brand);
|
|
21
|
+
--secondary: var(--theme-brand);
|
|
22
|
+
--accent: var(--theme-accent);
|
|
23
|
+
--primary-fg: var(--theme-on-brand);
|
|
24
|
+
--body-fg: var(--theme-text);
|
|
25
|
+
--body-bg: var(--theme-bg);
|
|
26
|
+
--body-quiet-color: var(--theme-muted);
|
|
27
|
+
--body-medium-color: var(--theme-muted);
|
|
28
|
+
--body-loud-color: var(--theme-text);
|
|
29
|
+
--header-color: var(--theme-muted);
|
|
30
|
+
--header-branding-color: var(--theme-text);
|
|
31
|
+
--header-bg: var(--theme-bg);
|
|
32
|
+
--header-link-color: var(--theme-brand);
|
|
33
|
+
--breadcrumbs-fg: var(--theme-muted);
|
|
34
|
+
--breadcrumbs-link-fg: var(--theme-brand);
|
|
35
|
+
--breadcrumbs-bg: var(--theme-surface);
|
|
36
|
+
--link-fg: var(--theme-brand);
|
|
37
|
+
--link-hover-color: var(--theme-accent);
|
|
38
|
+
--link-selected-fg: var(--theme-brand);
|
|
39
|
+
--hairline-color: var(--theme-border);
|
|
40
|
+
--border-color: var(--theme-border);
|
|
41
|
+
--error-fg: var(--theme-error);
|
|
42
|
+
--message-success-bg: var(--theme-soft);
|
|
43
|
+
--message-warning-bg: var(--theme-warning-bg);
|
|
44
|
+
--message-error-bg: var(--theme-error-bg);
|
|
45
|
+
--darkened-bg: var(--theme-surface);
|
|
46
|
+
--selected-bg: var(--theme-soft);
|
|
47
|
+
--selected-row: var(--theme-soft);
|
|
48
|
+
--button-fg: var(--theme-on-brand);
|
|
49
|
+
--button-bg: var(--theme-brand);
|
|
50
|
+
--button-hover-bg: #166445;
|
|
51
|
+
--default-button-bg: var(--theme-brand);
|
|
52
|
+
--default-button-hover-bg: #166445;
|
|
53
|
+
--close-button-bg: var(--theme-muted);
|
|
54
|
+
--close-button-hover-bg: var(--theme-text);
|
|
55
|
+
--delete-button-bg: var(--theme-error);
|
|
56
|
+
--delete-button-hover-bg: #82202a;
|
|
57
|
+
--object-tools-fg: var(--theme-on-brand);
|
|
58
|
+
--object-tools-bg: var(--theme-brand);
|
|
59
|
+
--object-tools-hover-bg: #166445;
|
|
60
|
+
--object-tools-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 4l4 4 4-4' fill='none' stroke='%23ffffff' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
61
|
+
--font-family-primary: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
62
|
+
--font-family-monospace: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
|
|
63
|
+
}
|
|
64
|
+
@media (prefers-color-scheme: dark) {
|
|
65
|
+
:root {
|
|
66
|
+
--theme-bg: #0d1512;
|
|
67
|
+
--theme-surface: #121d18;
|
|
68
|
+
--theme-text: #e7efeb;
|
|
69
|
+
--theme-muted: #9aaba3;
|
|
70
|
+
--theme-border: #22322b;
|
|
71
|
+
--theme-brand: #7fd8b1;
|
|
72
|
+
--theme-accent: #44b78b;
|
|
73
|
+
--theme-soft: #163327;
|
|
74
|
+
--theme-on-brand: #0d1512;
|
|
75
|
+
--theme-warning: #f5a54a;
|
|
76
|
+
--theme-warning-bg: #33230f;
|
|
77
|
+
--theme-error: #ff9da6;
|
|
78
|
+
--theme-error-bg: #351c22;
|
|
79
|
+
--button-hover-bg: #a1e5c6;
|
|
80
|
+
--default-button-hover-bg: #a1e5c6;
|
|
81
|
+
--object-tools-hover-bg: #a1e5c6;
|
|
82
|
+
--object-tools-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 4l4 4 4-4' fill='none' stroke='%230d1512' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
83
|
+
--delete-button-hover-bg: #ffc2c8;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
html[data-theme="dark"] {
|
|
87
|
+
--theme-bg: #0d1512;
|
|
88
|
+
--theme-surface: #121d18;
|
|
89
|
+
--theme-text: #e7efeb;
|
|
90
|
+
--theme-muted: #9aaba3;
|
|
91
|
+
--theme-border: #22322b;
|
|
92
|
+
--theme-brand: #7fd8b1;
|
|
93
|
+
--theme-accent: #44b78b;
|
|
94
|
+
--theme-soft: #163327;
|
|
95
|
+
--theme-on-brand: #0d1512;
|
|
96
|
+
--theme-warning: #f5a54a;
|
|
97
|
+
--theme-warning-bg: #33230f;
|
|
98
|
+
--theme-error: #ff9da6;
|
|
99
|
+
--theme-error-bg: #351c22;
|
|
100
|
+
--button-hover-bg: #a1e5c6;
|
|
101
|
+
--default-button-hover-bg: #a1e5c6;
|
|
102
|
+
--object-tools-hover-bg: #a1e5c6;
|
|
103
|
+
--object-tools-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 4l4 4 4-4' fill='none' stroke='%230d1512' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
104
|
+
--delete-button-hover-bg: #ffc2c8;
|
|
105
|
+
}
|
|
106
|
+
body { font-family: var(--font-family-primary); -webkit-font-smoothing: antialiased; }
|
|
107
|
+
h1, h2, h3, h4 { letter-spacing: -.015em; }
|
|
108
|
+
#content h1 { color: var(--theme-text); font-weight: 700; font-size: 1.7rem; }
|
|
109
|
+
#header {
|
|
110
|
+
background: var(--theme-bg);
|
|
111
|
+
border-bottom: 1px solid var(--theme-border);
|
|
112
|
+
padding: 20px 32px;
|
|
113
|
+
gap: 16px;
|
|
114
|
+
flex-wrap: wrap;
|
|
115
|
+
}
|
|
116
|
+
#site-name { margin: 0; font-size: 1.2rem; font-weight: 700; }
|
|
117
|
+
#site-name a { display: inline-flex; align-items: center; gap: 10px; color: var(--theme-text); }
|
|
118
|
+
|
|
119
|
+
#user-tools { color: var(--theme-muted); line-height: 1.7; }
|
|
120
|
+
#header a:link, #header a:visited { color: var(--theme-brand); }
|
|
121
|
+
#user-tools a, #logout-form button { border: 0; color: var(--theme-brand); }
|
|
122
|
+
div.breadcrumbs { padding: 14px 32px; border-bottom: 1px solid var(--theme-border); }
|
|
123
|
+
#content { padding: 28px 32px; }
|
|
124
|
+
nav .module, #content-main>.module {
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
border: 1px solid var(--theme-border);
|
|
127
|
+
border-radius: var(--theme-radius);
|
|
128
|
+
background: var(--theme-bg);
|
|
129
|
+
box-shadow: 0 12px 30px -26px rgba(12, 75, 51, .45);
|
|
130
|
+
}
|
|
131
|
+
.module h2, .module caption, .inline-group h2 {
|
|
132
|
+
background: var(--theme-surface);
|
|
133
|
+
color: var(--theme-text);
|
|
134
|
+
padding: 12px 16px;
|
|
135
|
+
font-weight: 600;
|
|
136
|
+
}
|
|
137
|
+
.module > h2, .module > table > caption, .inline-group > h2 {
|
|
138
|
+
border-radius: 13px 13px 0 0;
|
|
139
|
+
border-bottom: 1px solid var(--theme-border);
|
|
140
|
+
}
|
|
141
|
+
.module caption a:link, .module caption a:visited { color: var(--theme-brand); }
|
|
142
|
+
.module table { width: 100%; }
|
|
143
|
+
th, td { padding: 12px 14px; border-color: var(--theme-border); }
|
|
144
|
+
thead th {
|
|
145
|
+
background: var(--theme-surface);
|
|
146
|
+
color: var(--theme-muted);
|
|
147
|
+
font-size: .75rem;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
}
|
|
150
|
+
thead th.sorted { background: var(--theme-soft); }
|
|
151
|
+
.row1, .row2 { background: var(--theme-bg); }
|
|
152
|
+
#result_list tbody tr:hover { background: var(--theme-surface); }
|
|
153
|
+
#changelist { border-radius: var(--theme-radius); }
|
|
154
|
+
#changelist .results { overflow-x: auto; }
|
|
155
|
+
#changelist #toolbar { padding: 14px 16px; background: var(--theme-surface); border-color: var(--theme-border); }
|
|
156
|
+
#changelist .actions { background: var(--theme-bg); padding: 12px 16px; }
|
|
157
|
+
#changelist .paginator { background: var(--theme-surface); padding: 12px 16px; border-radius: 0 0 13px 13px; }
|
|
158
|
+
#changelist-filter { background: var(--theme-surface); border-inline-start: 1px solid var(--theme-border); }
|
|
159
|
+
#changelist-filter h2 { background: var(--theme-surface); color: var(--theme-text); }
|
|
160
|
+
#changelist-filter h3 { font-size: .9rem; color: var(--theme-text); }
|
|
161
|
+
#nav-sidebar { background: var(--theme-surface); border-color: var(--theme-border); padding: 12px 8px; }
|
|
162
|
+
#nav-sidebar .module { margin-bottom: 12px; }
|
|
163
|
+
#nav-sidebar th, #nav-sidebar td { padding: 10px; }
|
|
164
|
+
#nav-sidebar .current-model { background: var(--theme-soft); }
|
|
165
|
+
#toggle-nav-sidebar { background: var(--theme-bg); border-color: var(--theme-border); color: var(--theme-brand); }
|
|
166
|
+
input, textarea, select, button { font-family: inherit; }
|
|
167
|
+
input[type="text"], input[type="password"], input[type="email"], input[type="url"],
|
|
168
|
+
input[type="number"], input[type="date"], input[type="search"], input[type="tel"],
|
|
169
|
+
textarea, select, .vTextField, .vLargeTextField {
|
|
170
|
+
background: var(--theme-bg);
|
|
171
|
+
color: var(--theme-text);
|
|
172
|
+
border: 1px solid var(--theme-border);
|
|
173
|
+
border-radius: 8px;
|
|
174
|
+
padding: 9px 11px;
|
|
175
|
+
}
|
|
176
|
+
select { height: auto; min-height: 38px; line-height: 1.4; }
|
|
177
|
+
input[type="checkbox"], input[type="radio"] { accent-color: var(--theme-accent); }
|
|
178
|
+
a:focus-visible, button:focus-visible, input:focus-visible,
|
|
179
|
+
textarea:focus-visible, select:focus-visible {
|
|
180
|
+
outline: 2px solid var(--theme-accent);
|
|
181
|
+
outline-offset: 3px;
|
|
182
|
+
}
|
|
183
|
+
.button, input[type="submit"], input[type="button"], .submit-row input,
|
|
184
|
+
button[type="submit"] {
|
|
185
|
+
border: 1px solid transparent;
|
|
186
|
+
border-radius: 999px;
|
|
187
|
+
background: var(--theme-brand);
|
|
188
|
+
color: var(--theme-on-brand);
|
|
189
|
+
padding: 10px 18px;
|
|
190
|
+
font-weight: 600;
|
|
191
|
+
cursor: pointer;
|
|
192
|
+
}
|
|
193
|
+
.button:hover, input[type="submit"]:hover, input[type="button"]:hover,
|
|
194
|
+
button[type="submit"]:hover:hover { background: var(--button-hover-bg); }
|
|
195
|
+
#logout-form button { padding: 0; border: 0; border-radius: 0; background: transparent; color: var(--theme-brand); font-weight: inherit; }
|
|
196
|
+
/* Tool links and the actions select share one box. The selectors match
|
|
197
|
+
Django's own .object-tools a:link specificity so the padding applies. */
|
|
198
|
+
.object-tools a:link, .object-tools a:visited, .object-tools select {
|
|
199
|
+
display: block;
|
|
200
|
+
float: left;
|
|
201
|
+
box-sizing: border-box;
|
|
202
|
+
margin: 0;
|
|
203
|
+
padding: 8px 16px;
|
|
204
|
+
border: 1px solid transparent;
|
|
205
|
+
border-radius: 999px;
|
|
206
|
+
background-color: var(--object-tools-bg);
|
|
207
|
+
color: var(--object-tools-fg);
|
|
208
|
+
font-size: 0.6875rem;
|
|
209
|
+
font-weight: 600;
|
|
210
|
+
line-height: 1.25rem;
|
|
211
|
+
letter-spacing: 0.5px;
|
|
212
|
+
text-transform: uppercase;
|
|
213
|
+
}
|
|
214
|
+
.object-tools a:hover, .object-tools a:focus, .object-tools select:hover { background-color: var(--object-tools-hover-bg); }
|
|
215
|
+
.object-tools a.addlink, .object-tools a.viewsitelink { padding-right: 32px; background-position: right 12px center; }
|
|
216
|
+
.object-tools li { height: auto; margin-left: 10px; }
|
|
217
|
+
.object-tools select {
|
|
218
|
+
min-height: 0;
|
|
219
|
+
padding-right: 38px;
|
|
220
|
+
background-image: var(--object-tools-chevron);
|
|
221
|
+
background-repeat: no-repeat;
|
|
222
|
+
background-position: right 14px center;
|
|
223
|
+
appearance: none;
|
|
224
|
+
-webkit-appearance: none;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
}
|
|
227
|
+
.object-tools select:focus-visible { outline-offset: 2px; }
|
|
228
|
+
.object-tools select option { background: var(--theme-bg); color: var(--theme-text); font-weight: 400; text-transform: none; letter-spacing: 0; }
|
|
229
|
+
.submit-row { background: var(--theme-surface); border: 1px solid var(--theme-border); border-radius: var(--theme-radius); padding: 16px; }
|
|
230
|
+
.submit-row a.deletelink { border-radius: 999px; }
|
|
231
|
+
.form-row { padding: 16px; border-color: var(--theme-border); }
|
|
232
|
+
.form-row label { color: var(--theme-text); }
|
|
233
|
+
.help, p.help, .quiet { color: var(--theme-muted); }
|
|
234
|
+
.inline-group { border: 1px solid var(--theme-border); border-radius: var(--theme-radius); }
|
|
235
|
+
.inline-group .module { border: 0; border-radius: 0; box-shadow: none; }
|
|
236
|
+
.inline-group .tabular { overflow-x: auto; }
|
|
237
|
+
/* The row label is absolutely positioned inside td.original and sits at that
|
|
238
|
+
cell's static top; it must keep Django's 2px padding while the other cells
|
|
239
|
+
make room below it. */
|
|
240
|
+
.inline-group .tabular tr.has_original td { padding-top: 2.25rem; }
|
|
241
|
+
.inline-group .tabular tr.has_original td.original { padding-top: 2px; }
|
|
242
|
+
.inline-group .tabular tr.add-row td { background: var(--theme-soft); padding: 12px 16px; }
|
|
243
|
+
ul.messagelist li { border-radius: 10px; margin: 12px 0; color: var(--theme-text); }
|
|
244
|
+
.errornote, ul.errorlist { color: var(--theme-error); background: var(--theme-error-bg); border-color: var(--theme-error); }
|
|
245
|
+
.login #container { border: 1px solid var(--theme-border); border-radius: var(--theme-radius); box-shadow: 0 20px 50px -30px rgba(12, 75, 51, .45); }
|
|
246
|
+
.login #header { border-radius: 13px 13px 0 0; padding: 20px 24px; }
|
|
247
|
+
.login #content { padding: 24px; }
|
|
248
|
+
.login .submit-row { background: transparent; border: 0; }
|
|
249
|
+
.login .form-row input { box-sizing: border-box; }
|
|
250
|
+
@media (max-width: 767px) {
|
|
251
|
+
#header { padding: 16px; }
|
|
252
|
+
#content { padding: 20px 16px; }
|
|
253
|
+
div.breadcrumbs { padding: 12px 16px; }
|
|
254
|
+
#content h1 { font-size: 1.4rem; }
|
|
255
|
+
.form-row { padding: 12px; }
|
|
256
|
+
.submit-row { gap: 8px; }
|
|
257
|
+
.login #container { width: auto; max-width: 28rem; margin: 40px 16px; }
|
|
258
|
+
}
|
|
259
|
+
@media (prefers-reduced-motion: reduce) {
|
|
260
|
+
*, *::before, *::after { scroll-behavior: auto; }
|
|
261
|
+
}
|
|
262
|
+
.report-print-only { display: none; }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{% extends "admin/base.html" %}
|
|
2
|
+
{% load static %}
|
|
3
|
+
|
|
4
|
+
{% block responsive %}
|
|
5
|
+
{{ block.super }}
|
|
6
|
+
<link rel="stylesheet" href="{% static 'django_admin_grove/css/theme.css' %}">
|
|
7
|
+
<link rel="stylesheet" media="print" href="{% static 'django_admin_grove/css/print.css' %}">
|
|
8
|
+
{% block grove_styles %}{% endblock %}
|
|
9
|
+
{% endblock %}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-admin-grove
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A forest-green theme for Django admin, with light, dark, and print styles.
|
|
5
|
+
Author: Fabian Braun
|
|
6
|
+
Classifier: Framework :: Django
|
|
7
|
+
Classifier: Framework :: Django :: 5.2
|
|
8
|
+
Classifier: Framework :: Django :: 6.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: Django<6.1,>=5.2
|
|
13
|
+
|
|
14
|
+
# Django Admin Grove
|
|
15
|
+
|
|
16
|
+
A forest-green theme for Django admin with rounded surfaces, light and dark modes,
|
|
17
|
+
responsive layouts, and a print stylesheet. No JavaScript, external fonts, models,
|
|
18
|
+
or migrations are required. Uses Django's existing theme toggle and branding.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Install directly from Codeberg:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
python -m pip install git+https://codeberg.org/fsbraun/django-admin-grove
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Add Grove **before** Django's admin app (including custom AdminConfig subclasses):
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
INSTALLED_APPS = [
|
|
32
|
+
"django_admin_grove",
|
|
33
|
+
"django.contrib.admin",
|
|
34
|
+
# ...
|
|
35
|
+
]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Use Django's app template loader (`APP_DIRS = True`, or the explicit
|
|
39
|
+
`django.template.loaders.app_directories.Loader`) and staticfiles as usual.
|
|
40
|
+
Run `python manage.py collectstatic` for production deployments.
|
|
41
|
+
No URL configuration or database changes are needed. Django CMS is optional.
|
|
42
|
+
|
|
43
|
+
Supports Django 5.2 and 6.0; use a Python version supported by your Django release.
|
|
44
|
+
|
|
45
|
+
## Customization
|
|
46
|
+
|
|
47
|
+
Set `admin.site.site_header`, `site_title`, and `index_title` normally, or override
|
|
48
|
+
`admin/base_site.html` in your project. Grove leaves Django's branding blocks intact.
|
|
49
|
+
Project templates should extend `admin/base.html` and keep `block.super` when
|
|
50
|
+
replacing the `responsive` block.
|
|
51
|
+
|
|
52
|
+
Load project CSS after Grove using its `grove_styles` block:
|
|
53
|
+
|
|
54
|
+
```django
|
|
55
|
+
{% extends "admin/base.html" %}
|
|
56
|
+
{% load static %}
|
|
57
|
+
{% block grove_styles %}
|
|
58
|
+
<link rel="stylesheet" href="{% static 'myproject/admin.css' %}">
|
|
59
|
+
{% endblock %}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Override the `--theme-*` CSS variables for colors and surfaces. Match Grove's
|
|
63
|
+
`html[data-theme="light"]` and `html[data-theme="dark"]` selectors when customizing
|
|
64
|
+
both modes. System preferences apply when Django selects automatic mode.
|
|
65
|
+
The `--dca-primary` and `--dca-secondary` variables follow the active palette.
|
|
66
|
+
|
|
67
|
+
Printing uses A4 landscape and hides admin controls. Use `.report-screen-only`
|
|
68
|
+
to hide custom controls in print and `.report-print-only` for print-only context.
|
|
69
|
+
Domain-specific selectors and branding belong in the consuming project.
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
python -m pip install -e .
|
|
75
|
+
python -m django test tests --settings=tests.settings
|
|
76
|
+
python -m pip install build
|
|
77
|
+
python -m build
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Tests run against plain Django admin with an in-memory database.
|
|
81
|
+
|
|
82
|
+
## Publishing
|
|
83
|
+
|
|
84
|
+
Enable Actions in the Codeberg repository settings and make a runner with the
|
|
85
|
+
`codeberg-local` label available. Add a PyPI API token as the Actions secret
|
|
86
|
+
`PYPI_API_TOKEN`.
|
|
87
|
+
|
|
88
|
+
Update the version in `pyproject.toml`, then publish a Codeberg release with a
|
|
89
|
+
matching tag (for example, `0.1.0` or `v0.1.0`). The publication workflow checks
|
|
90
|
+
the version, runs the tests, builds and validates the wheel and source
|
|
91
|
+
distribution, and uploads both to PyPI.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
django_admin_grove/__init__.py
|
|
5
|
+
django_admin_grove/apps.py
|
|
6
|
+
django_admin_grove.egg-info/PKG-INFO
|
|
7
|
+
django_admin_grove.egg-info/SOURCES.txt
|
|
8
|
+
django_admin_grove.egg-info/dependency_links.txt
|
|
9
|
+
django_admin_grove.egg-info/requires.txt
|
|
10
|
+
django_admin_grove.egg-info/top_level.txt
|
|
11
|
+
django_admin_grove/static/django_admin_grove/css/print.css
|
|
12
|
+
django_admin_grove/static/django_admin_grove/css/theme.css
|
|
13
|
+
django_admin_grove/templates/admin/base.html
|
|
14
|
+
tests/__init__.py
|
|
15
|
+
tests/settings.py
|
|
16
|
+
tests/test_theme.py
|
|
17
|
+
tests/urls.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Django<6.1,>=5.2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
django_admin_grove
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "django-admin-grove"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A forest-green theme for Django admin, with light, dark, and print styles."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{name = "Fabian Braun"}]
|
|
12
|
+
dependencies = ["Django>=5.2,<6.1"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Framework :: Django",
|
|
15
|
+
"Framework :: Django :: 5.2",
|
|
16
|
+
"Framework :: Django :: 6.0",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
include = ["django_admin_grove*"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.package-data]
|
|
24
|
+
django_admin_grove = ["templates/admin/*.html", "static/django_admin_grove/css/*.css"]
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
SECRET_KEY = "grove-tests-only"
|
|
2
|
+
INSTALLED_APPS = [
|
|
3
|
+
"django_admin_grove",
|
|
4
|
+
"django.contrib.admin",
|
|
5
|
+
"django.contrib.auth",
|
|
6
|
+
"django.contrib.contenttypes",
|
|
7
|
+
"django.contrib.sessions",
|
|
8
|
+
"django.contrib.messages",
|
|
9
|
+
"django.contrib.staticfiles",
|
|
10
|
+
]
|
|
11
|
+
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}
|
|
12
|
+
ROOT_URLCONF = "tests.urls"
|
|
13
|
+
MIDDLEWARE = [
|
|
14
|
+
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
15
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
16
|
+
"django.contrib.messages.middleware.MessageMiddleware",
|
|
17
|
+
]
|
|
18
|
+
TEMPLATES = [{
|
|
19
|
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
20
|
+
"APP_DIRS": True,
|
|
21
|
+
"OPTIONS": {"context_processors": [
|
|
22
|
+
"django.template.context_processors.request",
|
|
23
|
+
"django.contrib.auth.context_processors.auth",
|
|
24
|
+
"django.contrib.messages.context_processors.messages",
|
|
25
|
+
]},
|
|
26
|
+
}]
|
|
27
|
+
STATIC_URL = "/static/"
|
|
28
|
+
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|
29
|
+
USE_TZ = True
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from django.contrib.auth import get_user_model
|
|
2
|
+
from django.contrib.staticfiles import finders
|
|
3
|
+
from django.test import TestCase
|
|
4
|
+
from django.urls import reverse
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ThemeTests(TestCase):
|
|
8
|
+
def test_anonymous_login_keeps_django_branding_and_theme_toggle(self):
|
|
9
|
+
response = self.client.get(reverse("admin:login"))
|
|
10
|
+
self.assertEqual(response.status_code, 200)
|
|
11
|
+
self.assertContains(response, "Django administration")
|
|
12
|
+
self.assertContains(response, "theme-toggle")
|
|
13
|
+
self.assertContains(response, "django_admin_grove/css/theme.css")
|
|
14
|
+
self.assertContains(response, "django_admin_grove/css/print.css")
|
|
15
|
+
self.assertNotContains(response, "Immoverse")
|
|
16
|
+
|
|
17
|
+
def test_authenticated_admin_keeps_navigation_and_assets(self):
|
|
18
|
+
user = get_user_model().objects.create_superuser("admin", password="test")
|
|
19
|
+
self.client.force_login(user)
|
|
20
|
+
for url in (reverse("admin:index"), reverse("admin:auth_user_changelist"),
|
|
21
|
+
reverse("admin:auth_user_add")):
|
|
22
|
+
with self.subTest(url=url):
|
|
23
|
+
response = self.client.get(url)
|
|
24
|
+
self.assertEqual(response.status_code, 200)
|
|
25
|
+
self.assertContains(response, "django_admin_grove/css/theme.css", count=1)
|
|
26
|
+
self.assertContains(response, "django_admin_grove/css/print.css", count=1)
|
|
27
|
+
self.assertContains(response, "theme-toggle")
|
|
28
|
+
|
|
29
|
+
def test_staticfiles_finds_both_stylesheets(self):
|
|
30
|
+
for filename in ("theme.css", "print.css"):
|
|
31
|
+
with self.subTest(filename=filename):
|
|
32
|
+
self.assertIsNotNone(finders.find("django_admin_grove/css/" + filename))
|