django-admin-modern-view 0.2.1__py3-none-any.whl
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_modern_view/__init__.py +1 -0
- django_admin_modern_view/apps.py +10 -0
- django_admin_modern_view/checks.py +55 -0
- django_admin_modern_view/services/__init__.py +0 -0
- django_admin_modern_view/services/command_registry.py +78 -0
- django_admin_modern_view/services/search.py +101 -0
- django_admin_modern_view/static/django_admin_modern_view/css/base.css +69 -0
- django_admin_modern_view/static/django_admin_modern_view/css/command-palette.css +151 -0
- django_admin_modern_view/static/django_admin_modern_view/css/forms.css +189 -0
- django_admin_modern_view/static/django_admin_modern_view/css/layout.css +315 -0
- django_admin_modern_view/static/django_admin_modern_view/css/sidebar.css +412 -0
- django_admin_modern_view/static/django_admin_modern_view/css/tables.css +402 -0
- django_admin_modern_view/static/django_admin_modern_view/css/utilities.css +236 -0
- django_admin_modern_view/static/django_admin_modern_view/css/variables.css +208 -0
- django_admin_modern_view/static/django_admin_modern_view/js/app.js +72 -0
- django_admin_modern_view/static/django_admin_modern_view/js/command-palette.js +226 -0
- django_admin_modern_view/static/django_admin_modern_view/js/dashboard.js +87 -0
- django_admin_modern_view/static/django_admin_modern_view/js/forms.js +0 -0
- django_admin_modern_view/static/django_admin_modern_view/js/global-search.js +29 -0
- django_admin_modern_view/static/django_admin_modern_view/js/notifications.js +58 -0
- django_admin_modern_view/static/django_admin_modern_view/js/shortcuts.js +76 -0
- django_admin_modern_view/static/django_admin_modern_view/js/sidebar.js +158 -0
- django_admin_modern_view/static/django_admin_modern_view/js/table.js +234 -0
- django_admin_modern_view/static/django_admin_modern_view/js/utils.js +105 -0
- django_admin_modern_view/templates/admin/base_site.html +114 -0
- django_admin_modern_view/templates/admin/index.html +58 -0
- django_admin_modern_view/templatetags/__init__.py +0 -0
- django_admin_modern_view/templatetags/django_admin_modern_view_tags.py +36 -0
- django_admin_modern_view/urls.py +10 -0
- django_admin_modern_view/views.py +18 -0
- django_admin_modern_view-0.2.1.dist-info/METADATA +192 -0
- django_admin_modern_view-0.2.1.dist-info/RECORD +35 -0
- django_admin_modern_view-0.2.1.dist-info/WHEEL +5 -0
- django_admin_modern_view-0.2.1.dist-info/licenses/LICENSE +21 -0
- django_admin_modern_view-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.1"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DjangoAdminModernViewConfig(AppConfig):
|
|
5
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
6
|
+
name = "django_admin_modern_view"
|
|
7
|
+
verbose_name = "Django Admin Modern View"
|
|
8
|
+
|
|
9
|
+
def ready(self):
|
|
10
|
+
from . import checks # noqa: F401 (registers Django system checks)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Django system checks that catch the one setup mistake this package
|
|
2
|
+
can't work around: app ordering.
|
|
3
|
+
|
|
4
|
+
Django's app_dirs template loader resolves ``admin/*.html`` overrides by
|
|
5
|
+
walking INSTALLED_APPS in order and using the first match. If
|
|
6
|
+
django_admin_modern_view isn't listed before django.contrib.admin, its template
|
|
7
|
+
overrides silently lose to Django's own — the admin just looks
|
|
8
|
+
unstyled, with no error to point at why.
|
|
9
|
+
"""
|
|
10
|
+
from django.apps import apps
|
|
11
|
+
from django.conf import settings
|
|
12
|
+
from django.core.checks import Warning, register
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register()
|
|
16
|
+
def check_app_order(app_configs, **kwargs):
|
|
17
|
+
installed = list(settings.INSTALLED_APPS)
|
|
18
|
+
try:
|
|
19
|
+
admin_index = next(
|
|
20
|
+
i for i, name in enumerate(installed)
|
|
21
|
+
if name == "django.contrib.admin" or name.startswith("django.contrib.admin.")
|
|
22
|
+
)
|
|
23
|
+
except StopIteration:
|
|
24
|
+
return [] # django.contrib.admin isn't installed; nothing to check
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
our_index = next(
|
|
28
|
+
i for i, name in enumerate(installed)
|
|
29
|
+
if name == "django_admin_modern_view" or name.startswith("django_admin_modern_view.")
|
|
30
|
+
)
|
|
31
|
+
except StopIteration:
|
|
32
|
+
our_index = None # resolved via AppConfig path instead of the bare name
|
|
33
|
+
|
|
34
|
+
if our_index is None:
|
|
35
|
+
try:
|
|
36
|
+
our_index = installed.index(apps.get_app_config("django_admin_modern_view").name)
|
|
37
|
+
except LookupError:
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
if our_index > admin_index:
|
|
41
|
+
return [
|
|
42
|
+
Warning(
|
|
43
|
+
"django_admin_modern_view is listed after django.contrib.admin in "
|
|
44
|
+
"INSTALLED_APPS.",
|
|
45
|
+
hint=(
|
|
46
|
+
"Move 'django_admin_modern_view' above 'django.contrib.admin' in "
|
|
47
|
+
"INSTALLED_APPS. Django's template loader picks the "
|
|
48
|
+
"first admin/*.html it finds by app order, so this "
|
|
49
|
+
"package's overrides (topbar, command palette, dashboard) "
|
|
50
|
+
"will otherwise be silently ignored."
|
|
51
|
+
),
|
|
52
|
+
id="django_admin_modern_view.W001",
|
|
53
|
+
)
|
|
54
|
+
]
|
|
55
|
+
return []
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Central registry for the Modern Admin command palette (Ctrl+K).
|
|
2
|
+
|
|
3
|
+
Commands are plain dicts so they serialize straight to JSON for the
|
|
4
|
+
frontend. Registered commands are merged with commands auto-derived
|
|
5
|
+
from the Django admin site (navigation + "create" actions) on every
|
|
6
|
+
request, so palette content always reflects the logged-in user's
|
|
7
|
+
permissions.
|
|
8
|
+
"""
|
|
9
|
+
from django.urls import reverse
|
|
10
|
+
|
|
11
|
+
_registered_commands = []
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register_command(id, label, category="General", shortcut=None, url=None, icon=None, keywords=""):
|
|
15
|
+
"""Register a custom command shown in the command palette.
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
register_command(
|
|
19
|
+
id="create_user",
|
|
20
|
+
label="Create User",
|
|
21
|
+
category="Actions",
|
|
22
|
+
url="/admin/auth/user/add/",
|
|
23
|
+
)
|
|
24
|
+
"""
|
|
25
|
+
_registered_commands.append({
|
|
26
|
+
"id": id,
|
|
27
|
+
"label": label,
|
|
28
|
+
"category": category,
|
|
29
|
+
"shortcut": shortcut,
|
|
30
|
+
"url": url,
|
|
31
|
+
"icon": icon or "",
|
|
32
|
+
"keywords": keywords,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_registered_commands():
|
|
37
|
+
return list(_registered_commands)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_commands_for_request(request):
|
|
41
|
+
"""Build the full palette command list for the current user."""
|
|
42
|
+
from django.contrib import admin
|
|
43
|
+
|
|
44
|
+
commands = [{
|
|
45
|
+
"id": "nav:dashboard",
|
|
46
|
+
"label": "Dashboard",
|
|
47
|
+
"category": "Navigation",
|
|
48
|
+
"shortcut": None,
|
|
49
|
+
"url": reverse("admin:index"),
|
|
50
|
+
"icon": "home",
|
|
51
|
+
"keywords": "home dashboard",
|
|
52
|
+
}]
|
|
53
|
+
|
|
54
|
+
for app in admin.site.get_app_list(request):
|
|
55
|
+
for model in app["models"]:
|
|
56
|
+
if model.get("admin_url"):
|
|
57
|
+
commands.append({
|
|
58
|
+
"id": f"nav:{app['app_label']}.{model['object_name']}",
|
|
59
|
+
"label": model["name"],
|
|
60
|
+
"category": "Navigation",
|
|
61
|
+
"shortcut": None,
|
|
62
|
+
"url": model["admin_url"],
|
|
63
|
+
"icon": "list",
|
|
64
|
+
"keywords": f"{app['name']} {model['name']}",
|
|
65
|
+
})
|
|
66
|
+
if model.get("add_url"):
|
|
67
|
+
commands.append({
|
|
68
|
+
"id": f"create:{app['app_label']}.{model['object_name']}",
|
|
69
|
+
"label": f"Create {model['name']}",
|
|
70
|
+
"category": "Actions",
|
|
71
|
+
"shortcut": None,
|
|
72
|
+
"url": model["add_url"],
|
|
73
|
+
"icon": "plus",
|
|
74
|
+
"keywords": f"add new {model['name']}",
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
commands.extend(get_registered_commands())
|
|
78
|
+
return commands
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Global search service (Phase 5).
|
|
2
|
+
|
|
3
|
+
Searches across models registered with the Django admin. Any
|
|
4
|
+
ModelAdmin that defines `search_fields` is auto-registered on app
|
|
5
|
+
startup so search works with zero extra config; `register_global_search`
|
|
6
|
+
lets you register a model explicitly (custom fields/label) or a model
|
|
7
|
+
that isn't registered with the admin at all.
|
|
8
|
+
|
|
9
|
+
Kept deliberately decoupled from the ORM implementation so it can
|
|
10
|
+
later be swapped for Postgres full text search / Meilisearch /
|
|
11
|
+
OpenSearch without touching the view or the frontend contract.
|
|
12
|
+
"""
|
|
13
|
+
from django.db.models import Q
|
|
14
|
+
|
|
15
|
+
_registry = []
|
|
16
|
+
_autodiscovered = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register_global_search(model, fields, label=None):
|
|
20
|
+
if any(entry["model"] is model for entry in _registry):
|
|
21
|
+
return
|
|
22
|
+
_registry.append({
|
|
23
|
+
"model": model,
|
|
24
|
+
"fields": list(fields),
|
|
25
|
+
"label": label or str(model._meta.verbose_name_plural).title(),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def autodiscover_from_admin():
|
|
30
|
+
"""Auto-register every ModelAdmin that defines search_fields.
|
|
31
|
+
|
|
32
|
+
Called lazily (on first search) rather than from AppConfig.ready():
|
|
33
|
+
django_admin_modern_view is listed before django.contrib.admin in
|
|
34
|
+
INSTALLED_APPS so that its template overrides win, but that also
|
|
35
|
+
means its ready() runs before admin.autodiscover() has populated
|
|
36
|
+
admin.site._registry.
|
|
37
|
+
"""
|
|
38
|
+
global _autodiscovered
|
|
39
|
+
from django.contrib import admin
|
|
40
|
+
|
|
41
|
+
for model, model_admin in admin.site._registry.items():
|
|
42
|
+
search_fields = getattr(model_admin, "search_fields", None)
|
|
43
|
+
if search_fields:
|
|
44
|
+
register_global_search(model, search_fields)
|
|
45
|
+
_autodiscovered = True
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _ensure_autodiscovered():
|
|
49
|
+
if not _autodiscovered:
|
|
50
|
+
autodiscover_from_admin()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _q_for_field(field, query):
|
|
54
|
+
if field.startswith("^"):
|
|
55
|
+
return Q(**{f"{field[1:]}__istartswith": query})
|
|
56
|
+
if field.startswith("="):
|
|
57
|
+
return Q(**{f"{field[1:]}__iexact": query})
|
|
58
|
+
if field.startswith("@"):
|
|
59
|
+
return Q(**{f"{field[1:]}__search": query})
|
|
60
|
+
return Q(**{f"{field}__icontains": query})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _admin_url_for(model, obj):
|
|
64
|
+
from django.urls import NoReverseMatch, reverse
|
|
65
|
+
|
|
66
|
+
opts = model._meta
|
|
67
|
+
try:
|
|
68
|
+
return reverse(f"admin:{opts.app_label}_{opts.model_name}_change", args=[obj.pk])
|
|
69
|
+
except NoReverseMatch:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def global_search(request, query, per_model_limit=5):
|
|
74
|
+
from django.contrib import admin
|
|
75
|
+
|
|
76
|
+
_ensure_autodiscovered()
|
|
77
|
+
results = []
|
|
78
|
+
for entry in _registry:
|
|
79
|
+
model = entry["model"]
|
|
80
|
+
model_admin = admin.site._registry.get(model)
|
|
81
|
+
if model_admin is not None and not model_admin.has_view_permission(request):
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
q_obj = Q()
|
|
85
|
+
for field in entry["fields"]:
|
|
86
|
+
q_obj |= _q_for_field(field, query)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
queryset = model._default_manager.filter(q_obj)[:per_model_limit]
|
|
90
|
+
except Exception:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
for obj in queryset:
|
|
94
|
+
results.append({
|
|
95
|
+
"type": entry["label"],
|
|
96
|
+
"label": str(obj),
|
|
97
|
+
"description": entry["label"],
|
|
98
|
+
"url": _admin_url_for(model, obj),
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
return results
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/* Global typography + surface tweaks layered on top of Django's admin/css/base.css */
|
|
2
|
+
|
|
3
|
+
body {
|
|
4
|
+
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
5
|
+
-webkit-font-smoothing: antialiased;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
a:link,
|
|
9
|
+
a:visited {
|
|
10
|
+
text-decoration: none;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
a:hover {
|
|
14
|
+
text-decoration: underline;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#content {
|
|
18
|
+
padding: 24px 32px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#content h1 {
|
|
22
|
+
font-size: 21px;
|
|
23
|
+
font-weight: 700;
|
|
24
|
+
letter-spacing: -0.01em;
|
|
25
|
+
color: var(--body-loud-color);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.breadcrumbs {
|
|
29
|
+
font-size: 12.5px;
|
|
30
|
+
padding: 10px 0 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.module {
|
|
34
|
+
border-radius: var(--radius-md);
|
|
35
|
+
box-shadow: var(--shadow-sm);
|
|
36
|
+
border: 1px solid var(--border-color);
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.module h2,
|
|
41
|
+
.module caption,
|
|
42
|
+
.inline-group h2 {
|
|
43
|
+
border-radius: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.button,
|
|
47
|
+
input[type="submit"],
|
|
48
|
+
input[type="button"],
|
|
49
|
+
.submit-row input,
|
|
50
|
+
a.button {
|
|
51
|
+
border-radius: var(--radius-sm);
|
|
52
|
+
transition: background-color 0.15s ease, box-shadow 0.15s ease;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.object-tools a,
|
|
56
|
+
.object-tools a:link {
|
|
57
|
+
border-radius: var(--radius-sm);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Focus visibility (Section 20: accessibility) */
|
|
61
|
+
a:focus-visible,
|
|
62
|
+
button:focus-visible,
|
|
63
|
+
input:focus-visible,
|
|
64
|
+
select:focus-visible,
|
|
65
|
+
textarea:focus-visible,
|
|
66
|
+
[tabindex]:focus-visible {
|
|
67
|
+
outline: 2px solid var(--primary);
|
|
68
|
+
outline-offset: 2px;
|
|
69
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/* Phase 4: command palette + generic modal/overlay styles */
|
|
2
|
+
|
|
3
|
+
.ma-overlay {
|
|
4
|
+
position: fixed;
|
|
5
|
+
inset: 0;
|
|
6
|
+
z-index: 1000;
|
|
7
|
+
display: none;
|
|
8
|
+
align-items: flex-start;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
padding-top: 12vh;
|
|
11
|
+
background: var(--overlay-bg);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.ma-overlay.ma-open {
|
|
15
|
+
display: flex;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.ma-palette {
|
|
19
|
+
width: 100%;
|
|
20
|
+
max-width: 600px;
|
|
21
|
+
max-height: 60vh;
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
background: var(--surface-raised);
|
|
25
|
+
border-radius: var(--radius-lg);
|
|
26
|
+
box-shadow: var(--shadow-lg);
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.ma-palette__input-row {
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: 10px;
|
|
34
|
+
padding: 14px 16px;
|
|
35
|
+
border-bottom: 1px solid var(--border-color);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ma-palette__input-row svg {
|
|
39
|
+
flex: 0 0 auto;
|
|
40
|
+
color: var(--body-quiet-color);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.ma-palette__input {
|
|
44
|
+
flex: 1 1 auto;
|
|
45
|
+
border: none;
|
|
46
|
+
background: transparent;
|
|
47
|
+
color: var(--body-fg);
|
|
48
|
+
font-size: 15px;
|
|
49
|
+
outline: none;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.ma-palette__results {
|
|
53
|
+
overflow-y: auto;
|
|
54
|
+
padding: 8px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.ma-palette__group-label {
|
|
58
|
+
padding: 8px 10px 4px;
|
|
59
|
+
font-size: 11px;
|
|
60
|
+
font-weight: 700;
|
|
61
|
+
letter-spacing: 0.05em;
|
|
62
|
+
text-transform: uppercase;
|
|
63
|
+
color: var(--body-quiet-color);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.ma-palette__item {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 10px;
|
|
70
|
+
padding: 9px 10px;
|
|
71
|
+
border-radius: var(--radius-sm);
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
color: var(--body-fg);
|
|
74
|
+
font-size: 13.5px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.ma-palette__item:hover,
|
|
78
|
+
.ma-palette__item.ma-active {
|
|
79
|
+
background: var(--selected-bg);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.ma-palette__item-icon {
|
|
83
|
+
flex: 0 0 auto;
|
|
84
|
+
width: 16px;
|
|
85
|
+
text-align: center;
|
|
86
|
+
color: var(--body-quiet-color);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.ma-palette__item-label {
|
|
90
|
+
flex: 1 1 auto;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.ma-palette__item-desc {
|
|
94
|
+
color: var(--body-quiet-color);
|
|
95
|
+
font-size: 12px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.ma-palette__empty {
|
|
99
|
+
padding: 24px;
|
|
100
|
+
text-align: center;
|
|
101
|
+
color: var(--body-quiet-color);
|
|
102
|
+
font-size: 13px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.ma-palette__footer {
|
|
106
|
+
display: flex;
|
|
107
|
+
gap: 14px;
|
|
108
|
+
padding: 8px 14px;
|
|
109
|
+
border-top: 1px solid var(--border-color);
|
|
110
|
+
font-size: 11px;
|
|
111
|
+
color: var(--body-quiet-color);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Generic modal (shortcut help, confirmations) */
|
|
115
|
+
.ma-modal {
|
|
116
|
+
width: 100%;
|
|
117
|
+
max-width: 480px;
|
|
118
|
+
background: var(--surface-raised);
|
|
119
|
+
border-radius: var(--radius-lg);
|
|
120
|
+
box-shadow: var(--shadow-lg);
|
|
121
|
+
padding: 20px 22px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.ma-modal__title {
|
|
125
|
+
margin: 0 0 14px;
|
|
126
|
+
font-size: 15px;
|
|
127
|
+
font-weight: 600;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.ma-shortcut-row {
|
|
131
|
+
display: flex;
|
|
132
|
+
justify-content: space-between;
|
|
133
|
+
padding: 6px 0;
|
|
134
|
+
font-size: 13px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.ma-shortcut-row + .ma-shortcut-row {
|
|
138
|
+
border-top: 1px solid var(--hairline-color);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.ma-shortcut-group + .ma-shortcut-group {
|
|
142
|
+
margin-top: 14px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.ma-shortcut-group__title {
|
|
146
|
+
font-size: 11px;
|
|
147
|
+
font-weight: 700;
|
|
148
|
+
letter-spacing: 0.05em;
|
|
149
|
+
text-transform: uppercase;
|
|
150
|
+
color: var(--body-quiet-color);
|
|
151
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/* Phase 8: enhanced forms — sticky save bar + dirty-state pill.
|
|
2
|
+
Works with Django's existing .submit-row, no template override. */
|
|
3
|
+
|
|
4
|
+
.submit-row {
|
|
5
|
+
position: sticky;
|
|
6
|
+
/* Stop above the fixed .ma-statusbar (layout.css) instead of
|
|
7
|
+
underneath/behind it — both pinning to bottom:0 made them
|
|
8
|
+
overlap. */
|
|
9
|
+
bottom: var(--statusbar-height);
|
|
10
|
+
z-index: 100;
|
|
11
|
+
border-radius: var(--radius-md);
|
|
12
|
+
box-shadow: var(--shadow-md);
|
|
13
|
+
align-items: center;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.ma-dirty-indicator {
|
|
17
|
+
display: none;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 6px;
|
|
20
|
+
margin-right: auto;
|
|
21
|
+
padding-left: 4px;
|
|
22
|
+
font-size: 12.5px;
|
|
23
|
+
color: var(--warning);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.ma-dirty-indicator::before {
|
|
27
|
+
content: "";
|
|
28
|
+
width: 7px;
|
|
29
|
+
height: 7px;
|
|
30
|
+
border-radius: 50%;
|
|
31
|
+
background: currentColor;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.submit-row.ma-dirty .ma-dirty-indicator {
|
|
35
|
+
display: flex;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fieldset.module {
|
|
39
|
+
margin-bottom: 16px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.form-row {
|
|
43
|
+
border-bottom-color: var(--hairline-color);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Modern text inputs / textareas / selects. Excludes buttons and the
|
|
47
|
+
checkbox/radio/file inputs, which keep their native controls. */
|
|
48
|
+
input:not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]),
|
|
49
|
+
textarea,
|
|
50
|
+
select,
|
|
51
|
+
.vTextField,
|
|
52
|
+
.vDateField,
|
|
53
|
+
.vTimeField,
|
|
54
|
+
.vURLField {
|
|
55
|
+
background: var(--surface-raised);
|
|
56
|
+
border: 1px solid var(--border-color);
|
|
57
|
+
border-radius: var(--radius-sm);
|
|
58
|
+
padding: 8px 10px;
|
|
59
|
+
font-size: 13.5px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
select {
|
|
63
|
+
padding-top: 6px;
|
|
64
|
+
padding-bottom: 6px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
select[multiple] {
|
|
68
|
+
padding: 6px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
input:focus,
|
|
72
|
+
textarea:focus,
|
|
73
|
+
select:focus {
|
|
74
|
+
border-color: var(--primary);
|
|
75
|
+
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15);
|
|
76
|
+
outline: none;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
html[data-theme="dark"] input:focus,
|
|
80
|
+
html[data-theme="dark"] textarea:focus,
|
|
81
|
+
html[data-theme="dark"] select:focus {
|
|
82
|
+
box-shadow: 0 0 0 3px rgba(129, 140, 248, 0.25);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.errorlist {
|
|
86
|
+
border-radius: var(--radius-sm);
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Related-object widgets: FK <select> + add/change/view icon buttons */
|
|
91
|
+
.related-widget-wrapper {
|
|
92
|
+
align-items: center;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.related-widget-wrapper select {
|
|
96
|
+
min-width: 260px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.related-widget-wrapper-link {
|
|
100
|
+
display: inline-flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
justify-content: center;
|
|
103
|
+
width: 28px;
|
|
104
|
+
height: 28px;
|
|
105
|
+
border-radius: var(--radius-sm);
|
|
106
|
+
background: var(--darkened-bg);
|
|
107
|
+
border: 1px solid var(--border-color);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.related-widget-wrapper-link:hover {
|
|
111
|
+
background: var(--selected-bg);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.related-widget-wrapper-link img {
|
|
115
|
+
width: 14px;
|
|
116
|
+
height: 14px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Filter-horizontal / filter-vertical (m2m selector) */
|
|
120
|
+
.selector-available-title,
|
|
121
|
+
.selector-chosen-title {
|
|
122
|
+
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
|
123
|
+
border: 1px solid var(--border-color);
|
|
124
|
+
border-bottom: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.selector-chosen-title {
|
|
128
|
+
background: var(--primary);
|
|
129
|
+
color: var(--primary-fg);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.selector-chosen-title label {
|
|
133
|
+
color: var(--primary-fg);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.selector-available-title {
|
|
137
|
+
background: var(--darkened-bg);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.selector .selector-filter {
|
|
141
|
+
background: var(--surface-raised);
|
|
142
|
+
border-color: var(--border-color);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.selector select {
|
|
146
|
+
background: var(--surface-raised);
|
|
147
|
+
border: 1px solid var(--border-color);
|
|
148
|
+
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.selector ul.selector-chooser {
|
|
152
|
+
background: var(--surface-raised);
|
|
153
|
+
border: 1px solid var(--border-color);
|
|
154
|
+
border-radius: var(--radius-lg);
|
|
155
|
+
box-shadow: var(--shadow-sm);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.selector-add,
|
|
159
|
+
.selector-remove {
|
|
160
|
+
border-radius: var(--radius-sm);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.selector-chosen .list-footer-display {
|
|
164
|
+
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Icon-only chrome (related-object icons, calendar/clock shortcuts, search
|
|
168
|
+
lookup icon) ships as dark-stroke SVGs meant for a light page — Django's
|
|
169
|
+
own dark_mode.css doesn't invert them, so they go nearly invisible on a
|
|
170
|
+
dark surface. Brighten them only in dark mode. */
|
|
171
|
+
html[data-theme="dark"] .related-widget-wrapper-link img,
|
|
172
|
+
html[data-theme="dark"] .related-lookup,
|
|
173
|
+
html[data-theme="dark"] .selector-add,
|
|
174
|
+
html[data-theme="dark"] .selector-remove,
|
|
175
|
+
html[data-theme="dark"] .datetimeshortcuts img,
|
|
176
|
+
html[data-theme="dark"] .calendar-shortcuts img {
|
|
177
|
+
filter: invert(1) brightness(1.5);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@media (prefers-color-scheme: dark) {
|
|
181
|
+
:root:not([data-theme="light"]) .related-widget-wrapper-link img,
|
|
182
|
+
:root:not([data-theme="light"]) .related-lookup,
|
|
183
|
+
:root:not([data-theme="light"]) .selector-add,
|
|
184
|
+
:root:not([data-theme="light"]) .selector-remove,
|
|
185
|
+
:root:not([data-theme="light"]) .datetimeshortcuts img,
|
|
186
|
+
:root:not([data-theme="light"]) .calendar-shortcuts img {
|
|
187
|
+
filter: invert(1) brightness(1.5);
|
|
188
|
+
}
|
|
189
|
+
}
|