'Prefers-contrast' admin theming (#12348)

Co-authored-by: Victoria Ottah <82820329+Toriasdesign@users.noreply.github.com>
pull/12400/head
Albina 2024-10-17 10:13:22 +02:00 zatwierdzone przez GitHub
rodzic 576eaf37b2
commit 488c3583b7
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
37 zmienionych plików z 287 dodań i 21 usunięć

Wyświetl plik

@ -20,8 +20,9 @@ Changelog
* Fire `copy_for_translation_done` signal when copying translatable models as well as pages (Coen van der Kamp)
* Add support for an image `description` field across all images, to better support accessible image descriptions (Chiemezuo Akujobi)
* Prompt the user about unsaved changes when editing snippets (Sage Abdullah)
* Implement incremental dashboard design enhancements (Albina Starykova)
* Implement incremental dashboard design enhancements (Albina Starykova, Ben Enright)
* Add support for specifying different preview modes to the "View draft" URL for pages (Robin Varghese)
* Add a new enhanced contrast admin theming option for the admin interface (Albina Starykova, Victoria Ottah)
* Fix: Prevent page type business rules from blocking reordering of pages (Andy Babic, Sage Abdullah)
* Fix: Improve layout of object permissions table (Sage Abdullah)
* Fix: Fix typo in aria-label attribute of page explorer navigation link (Sébastien Corbin)

Wyświetl plik

@ -840,6 +840,7 @@
* Gabriel Getzie
* Rohit Singh
* Robin Varghese
* Victoria Ottah
## Translators

Wyświetl plik

@ -8,6 +8,10 @@
.form-side--checks & {
display: block;
}
@include more-contrast() {
border-color: theme('colors.border-furniture-more-contrast');
}
}
.w-a11y-result__header {

Wyświetl plik

@ -321,11 +321,13 @@
}
.w-header-button {
@include more-contrast-interactive();
display: flex;
align-items: center;
justify-content: center;
gap: theme('spacing.1');
height: theme('spacing.7');
height: theme('spacing.8');
min-width: theme('spacing.8');
appearance: none;
background-color: initial;
border: 1px solid transparent;

Wyświetl plik

@ -15,6 +15,7 @@ $preview-size: 2.625rem; // 42px
// Very subdued button style specifically for choosers, as there can be a lot of
// chooser fields left unused on a page editing form.
.button.chooser__choose-button {
@include more-contrast-interactive();
@apply w-label-3;
display: flex;
align-items: center;
@ -81,11 +82,6 @@ $preview-size: 2.625rem; // 42px
width: auto;
}
.chooser .w-dropdown__toggle--icon {
width: $preview-size;
height: $preview-size;
}
// Display these as inline block so that action icons such as comments can appear as close as possible
.w-field--admin_task_chooser,
.w-field--admin_page_chooser,

Wyświetl plik

@ -3,7 +3,7 @@
}
.w-dropdown__toggle--icon {
@apply w-w-8 w-h-8;
@apply w-w-8 w-h-8 more-contrast:w-p-0 more-contrast:w-border more-contrast:w-rounded-sm more-contrast:w-border-border-interactive-more-contrast hover:more-contrast:w-border-border-interactive-more-contrast-hover;
}
.w-dropdown__toggle-icon {

Wyświetl plik

@ -25,6 +25,7 @@
}
.w-filter-button {
@include more-contrast-interactive();
position: relative;
width: theme('spacing.10');
height: theme('spacing.10');

Wyświetl plik

@ -36,7 +36,8 @@
sm:w-max-w-[22.5rem]
md:w-max-w-[35.937rem]
lg:w-max-w-[31.25rem]
xl:w-max-w-[46.875rem];
xl:w-max-w-[46.875rem]
more-contrast:w-border-border-furniture-more-contrast;
z-index: calc(theme('zIndex.header') - 10);
width: var(--side-panel-width, 100%);

Wyświetl plik

@ -151,6 +151,10 @@
}
.w-slim-header {
@include more-contrast() {
border-color: theme('colors.border-furniture-more-contrast');
}
&__search-form {
@apply w-mx-2 w-flex w-items-center w-gap-2;
}

Wyświetl plik

@ -58,6 +58,10 @@
.warning {
background-color: theme('colors.warning.100');
color: theme('colors.grey.600');
@include more-contrast() {
background-color: theme('colors.warning.75');
}
}
.info {

Wyświetl plik

@ -61,6 +61,7 @@ $header-button-size: theme('spacing.6');
.w-panel__toggle,
.w-panel__controls .button.button--icon {
@include show-focus-outline-inside();
@include more-contrast-interactive();
display: inline-grid;
justify-content: center;
align-content: center;

Wyświetl plik

@ -59,6 +59,10 @@
gap: 0.75rem;
padding-bottom: 1rem;
margin-bottom: 1rem;
@include more-contrast() {
border-color: theme('colors.border-furniture-more-contrast');
}
}
&__size-button {

Wyświetl plik

@ -3,6 +3,7 @@
* Text input, textarea, checkbox, radio, select, etc.
*/
@mixin input-base() {
@include more-contrast-interactive();
appearance: none;
border-radius: theme('borderRadius.DEFAULT');
color: theme('colors.text-context');

Wyświetl plik

@ -93,6 +93,21 @@
}
}
/**
* Apply styles for enhanced contrast theming.
*/
@mixin more-contrast() {
.w-contrast-more & {
@content;
}
@media (prefers-contrast: more) {
.w-contrast-system & {
@content;
}
}
}
/**
* Apply styles for the light theme only.
*/
@ -107,3 +122,48 @@
}
}
}
/**
* Apply styles for the dark theme with increased contrast.
*/
@mixin dark-theme-more-contrast() {
.w-theme-dark.w-contrast-more & {
@content;
}
@media (prefers-color-scheme: dark) {
.w-theme-system.w-contrast-more & {
@content;
}
}
@media (prefers-contrast: more) {
.w-theme-dark.w-contrast-system & {
@content;
}
}
@media (prefers-color-scheme: dark) and (prefers-contrast: more) {
.w-theme-system.w-contrast-system & {
@content;
}
}
}
/**
* Increased contrast theme styles for interactive components
*/
@mixin more-contrast-interactive() {
@include more-contrast() {
border: 1px solid theme('colors.border-interactive-more-contrast');
&:hover {
border-color: theme('colors.border-interactive-more-contrast-hover');
}
&[disabled],
&[disabled]:hover {
border-style: dashed;
}
}
}

Wyświetl plik

@ -1,5 +1,6 @@
.comment {
@include box;
@include more-contrast-interactive();
width: 300px;
display: block;

Wyświetl plik

@ -40,6 +40,8 @@
> button,
> details > summary {
@include more-contrast-interactive();
border-radius: theme('borderRadius.sm');
list-style-type: none; // Hides triangle on Firefox
width: 30px;
height: 30px;

Wyświetl plik

@ -101,6 +101,11 @@ $draftail-editor-font-family: theme('fontFamily.sans');
background-color: $draftail-editor-background;
color: $draftail-placeholder-text;
&--pin {
display: flex;
flex-wrap: wrap;
}
.Draftail-Editor--focus & {
color: $draftail-editor-text;
top: calc(theme('spacing.slim-header') * 2);
@ -205,11 +210,19 @@ $draftail-editor-font-family: theme('fontFamily.sans');
}
}
.Draftail-ToolbarGroup {
display: flex;
}
.Draftail-ToolbarGroup::before {
display: none;
}
.Draftail-ToolbarButton {
@include more-contrast-interactive();
display: flex;
align-items: center;
justify-content: center;
height: 1.875rem;
min-width: 1.875rem;
padding: 0;
@ -257,6 +270,7 @@ $draftail-editor-font-family: theme('fontFamily.sans');
.Draftail-Toolbar & {
border-color: theme('colors.border-field-default');
background-color: theme('colors.surface-page');
color: currentColor;
border-top-width: 0;
border-inline-end-width: 0;

Wyświetl plik

@ -1,4 +1,5 @@
.w-minimap__collapse-all {
@include more-contrast-interactive();
display: none;
// Keep the icon at a stable position and reduce the amount of shifting of the button.
min-width: 110px;

Wyświetl plik

@ -35,6 +35,10 @@
@media (forced-colors: active) {
border-inline-start-width: 3px;
}
@include more-contrast() {
border-inline-start-width: 3px;
}
}
&:hover {

Wyświetl plik

@ -45,6 +45,9 @@ $sidebar-toggle-size: 35px;
@media (forced-colors: active) {
border-inline-end: 1px solid transparent;
}
@include dark-theme-more-contrast() {
border-inline-end: 1px solid theme('colors.border-furniture-more-contrast');
}
.icon--menuitem {
width: 1rem;

Wyświetl plik

@ -230,7 +230,9 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
w-items-center
hover:w-bg-surface-menu-item-active
hover:text-white
hover:opacity-100`}
hover:opacity-100
more-contrast:w-border-border-interactive-more-contrast-dark-bg
hover:more-contrast:w-border-border-interactive-more-contrast-dark-bg-hover`}
>
<Icon
name="expand-right"

Wyświetl plik

@ -35,7 +35,9 @@ exports[`Sidebar should render with the minimum required props 1`] = `
w-items-center
hover:w-bg-surface-menu-item-active
hover:text-white
hover:opacity-100"
hover:opacity-100
more-contrast:w-border-border-interactive-more-contrast-dark-bg
hover:more-contrast:w-border-border-interactive-more-contrast-dark-bg-hover"
onClick={[Function]}
type="button"
>

Wyświetl plik

@ -295,6 +295,36 @@ const light = [
textUtility: 'w-text-border-button-outline-hover',
cssVariable: '--w-color-border-button-outline-hover',
},
'border-interactive-more-contrast': {
value: 'var(--w-color-grey-500)',
bgUtility: 'w-bg-border-interactive-more-contrast',
textUtility: 'w-text-border-interactive-more-contrast',
cssVariable: '--w-color-border-interactive-more-contrast',
},
'border-interactive-more-contrast-hover': {
value: 'var(--w-color-black)',
bgUtility: 'w-bg-border-interactive-more-contrast-hover',
textUtility: 'w-text-border-interactive-more-contrast-hover',
cssVariable: '--w-color-border-interactive-more-contrast-hover',
},
'border-interactive-more-contrast-dark-bg': {
value: 'var(--w-color-grey-150)',
bgUtility: 'w-bg-border-interactive-more-contrast-dark-bg',
textUtility: 'w-text-border-interactive-more-contrast-dark-bg',
cssVariable: '--w-color-border-interactive-more-contrast-dark-bg',
},
'border-interactive-more-contrast-dark-bg-hover': {
value: 'var(--w-color-white)',
bgUtility: 'w-bg-border-interactive-more-contrast-dark-bg-hover',
textUtility: 'w-text-border-interactive-more-contrast-dark-bg-hover',
cssVariable: '--w-color-border-interactive-more-contrast-dark-bg-hover',
},
'border-furniture-more-contrast': {
value: 'var(--w-color-grey-200)',
bgUtility: 'w-bg-border-furniture-more-contrast',
textUtility: 'w-text-border-furniture-more-contrast',
cssVariable: '--w-color-border-furniture-more-contrast',
},
},
},
{
@ -583,6 +613,36 @@ const dark = [
textUtility: 'w-text-border-button-outline-hover',
cssVariable: '--w-color-border-button-outline-hover',
},
'border-interactive-more-contrast': {
value: 'var(--w-color-grey-150)',
bgUtility: 'w-bg-border-interactive-more-contrast',
textUtility: 'w-text-border-interactive-more-contrast',
cssVariable: '--w-color-border-interactive-more-contrast',
},
'border-interactive-more-contrast-hover': {
value: 'var(--w-color-white)',
bgUtility: 'w-bg-border-interactive-more-contrast-hover',
textUtility: 'w-text-border-interactive-more-contrast-hover',
cssVariable: '--w-color-border-interactive-more-contrast-hover',
},
'border-interactive-more-contrast-dark-bg': {
value: 'var(--w-color-grey-150)',
bgUtility: 'w-bg-border-interactive-more-contrast-dark-bg',
textUtility: 'w-text-border-interactive-more-contrast-dark-bg',
cssVariable: '--w-color-border-interactive-more-contrast-dark-bg',
},
'border-interactive-more-contrast-dark-bg-hover': {
value: 'var(--w-color-white)',
bgUtility: 'w-bg-border-interactive-more-contrast-dark-bg-hover',
textUtility: 'w-text-border-interactive-more-contrast-dark-bg-hover',
cssVariable: '--w-color-border-interactive-more-contrast-dark-bg-hover',
},
'border-furniture-more-contrast': {
value: 'var(--w-color-grey-400)',
bgUtility: 'w-bg-border-furniture-more-contrast',
textUtility: 'w-text-border-furniture-more-contrast',
cssVariable: '--w-color-border-furniture-more-contrast',
},
},
},
{

Wyświetl plik

@ -143,6 +143,10 @@ describe('generateColorVariables', () => {
"--w-color-warning-50-hue": "calc(var(--w-color-warning-100-hue) - 2.3)",
"--w-color-warning-50-lightness": "calc(var(--w-color-warning-100-lightness) + 41.8%)",
"--w-color-warning-50-saturation": "calc(var(--w-color-warning-100-saturation) - 21.3%)",
"--w-color-warning-75": "hsl(var(--w-color-warning-75-hue) var(--w-color-warning-75-saturation) var(--w-color-warning-75-lightness))",
"--w-color-warning-75-hue": "calc(var(--w-color-warning-100-hue) + 0.7)",
"--w-color-warning-75-lightness": "calc(var(--w-color-warning-100-lightness) + 23.4%)",
"--w-color-warning-75-saturation": "calc(var(--w-color-warning-100-saturation) - 2.8%)",
"--w-color-white": "hsl(var(--w-color-white-hue) var(--w-color-white-saturation) var(--w-color-white-lightness))",
"--w-color-white-hue": "0",
"--w-color-white-lightness": "100%",
@ -200,6 +204,11 @@ describe('generateThemeColorVariables', () => {
"--w-color-border-field-hover": "var(--w-color-grey-200)",
"--w-color-border-field-inactive": "var(--w-color-grey-150)",
"--w-color-border-furniture": "var(--w-color-grey-100)",
"--w-color-border-furniture-more-contrast": "var(--w-color-grey-200)",
"--w-color-border-interactive-more-contrast": "var(--w-color-grey-500)",
"--w-color-border-interactive-more-contrast-dark-bg": "var(--w-color-grey-150)",
"--w-color-border-interactive-more-contrast-dark-bg-hover": "var(--w-color-white)",
"--w-color-border-interactive-more-contrast-hover": "var(--w-color-black)",
"--w-color-box-shadow-md": "var(--w-color-black-25)",
"--w-color-focus": "#00A885",
"--w-color-icon-primary": "var(--w-color-primary)",
@ -252,6 +261,11 @@ describe('generateThemeColorVariables', () => {
"--w-color-border-field-hover": "var(--w-color-grey-200)",
"--w-color-border-field-inactive": "var(--w-color-grey-500)",
"--w-color-border-furniture": "var(--w-color-grey-500)",
"--w-color-border-furniture-more-contrast": "var(--w-color-grey-400)",
"--w-color-border-interactive-more-contrast": "var(--w-color-grey-150)",
"--w-color-border-interactive-more-contrast-dark-bg": "var(--w-color-grey-150)",
"--w-color-border-interactive-more-contrast-dark-bg-hover": "var(--w-color-white)",
"--w-color-border-interactive-more-contrast-hover": "var(--w-color-white)",
"--w-color-box-shadow-md": "var(--w-color-black-50)",
"--w-color-focus": "#00A885",
"--w-color-icon-primary": "var(--w-color-grey-150)",

Wyświetl plik

@ -268,6 +268,16 @@ const staticColors = {
usage: 'Background and icons for potentially dangerous states',
contrastText: 'primary',
},
75: {
hex: '#FDD074',
hsl: 'hsl(40.3, 97.2%, 72.4%)',
bgUtility: 'w-bg-warning-75',
textUtility: 'w-text-warning-75',
cssVariable: '--w-color-warning-75',
usage:
'Background only, for potentially dangerous states, in enhanced-contrast theme',
contrastText: 'primary',
},
50: {
hex: '#FFF5D8',
hsl: 'hsl(37.3 78.7% 90.8%)',

Wyświetl plik

@ -190,6 +190,13 @@ module.exports = {
plugin(({ addVariant }) => {
addVariant('expanded', '&[aria-expanded=true]');
}),
/** Support for increased contrast theme */
plugin(({ addVariant }) => {
addVariant('more-contrast', [
'.contrast-more &',
'@media (prefers-contrast: more) { .contrast-system & }',
]);
}),
],
corePlugins: {
...vanillaRTL.disabledCorePlugins,

Wyświetl plik

@ -25,6 +25,13 @@ The Wagtail dashboard design evolves towards providing more information and navi
This feature was developed by Albina Starykova based on designs by Ben Enright.
### Enhanced contrast admin theme
CMS users can now control the level of contrast of UI elements in the admin interface.
This new customization is designed for partially sighted users, complementing existing support for a dark theme and Windows Contrast Themes.
The new "More contrast" theming can be enabled in account preferences, or will otherwise be derived from operating system preferences.
This feature was designed thanks to feedback from our blind and partially sighted users, and was developed by Albina Starykova based on design input from Victoria Ottah.
### Other features

Wyświetl plik

@ -140,4 +140,4 @@ class AvatarPreferencesForm(forms.ModelForm):
class ThemePreferencesForm(forms.ModelForm):
class Meta:
model = UserProfile
fields = ["theme", "density"]
fields = ["theme", "contrast", "density"]

Wyświetl plik

@ -2,7 +2,7 @@
{% trans "Actions" as title %}
<nav aria-label="{{ title }}">
{% dropdown toggle_icon="dots-horizontal" toggle_aria_label=title toggle_classname="w-p-0 w-w-8 w-h-slim-header hover:w-scale-110 w-transition w-outline-offset-inside w-relative w-z-30" toggle_tooltip_offset="[0, -2]" %}
{% dropdown toggle_icon="dots-horizontal" toggle_aria_label=title toggle_classname="w-p-0 w-outline-offset-inside w-relative w-z-30" toggle_tooltip_offset="[0, -2]" %}
{% block content %}
{% include "wagtailadmin/pages/listing/_dropdown_items.html" with buttons=buttons only %}
{% endblock %}

Wyświetl plik

@ -19,7 +19,7 @@
{% endcomment %}
{% fragment as nav_icon_classes %}w-w-4 w-h-4 group-hover:w-transform group-hover:w-scale-110{% endfragment %}
{% fragment as nav_icon_button_classes %}w-w-slim-header w-h-slim-header w-bg-transparent w-border-transparent w-box-border w-py-3 w-px-3 w-flex w-justify-center w-items-center w-outline-offset-inside w-text-text-meta w-transition w-group hover:w-text-text-label focus:w-text-text-label expanded:w-text-text-label expanded:w-border-y-2 expanded:w-border-b-current w-shrink-0{% endfragment %}
{% fragment as nav_icon_button_classes %}w-w-slim-header w-h-slim-header w-bg-transparent w-border-transparent w-box-border w-py-3 w-px-3 w-flex w-justify-center w-items-center w-outline-offset-inside w-text-text-meta w-transition w-group hover:w-text-text-label focus:w-text-text-label expanded:w-text-text-label expanded:w-border-y-2 expanded:w-border-b-current w-shrink-0 more-contrast:w-border more-contrast:w-border-border-interactive-more-contrast hover:more-contrast:w-border-border-interactive-more-contrast-hover{% endfragment %}
{% fragment as nav_icon_counter_classes %}-w-mr-3 w-py-0.5 w-px-[0.325rem] w-translate-y-[-8px] rtl:w-translate-x-[4px] w-translate-x-[-4px] w-text-[0.5625rem] w-font-bold w-text-text-button w-border w-border-surface-page w-rounded-[1rem]{% endfragment %}
{# Z index 99 to ensure header is always above #}
<div class="w-sticky w-top-0 w-z-header">

Wyświetl plik

@ -31,7 +31,10 @@
w-font-semibold
hover:w-border-surface-menus
hover:w-text-text-label
w-transition"
w-transition
more-contrast:w-border
more-contrast:w-border-border-interactive-more-contrast
hover:more-contrast:w-border-border-interactive-more-contrast-hover"
aria-label="{% if is_public %}{% trans 'Visible to all. Visit the live page' %}{% else %}{% trans 'Private. Visit the live page' %}{% endif %}"
data-controller="w-tooltip"
data-w-tooltip-content-value="{% if is_public %}{% trans 'Visible to all' %}{% else %}{% trans 'Private' %}{% endif %}"

Wyświetl plik

@ -683,12 +683,18 @@ def admin_theme_classname(context):
if hasattr(user, "wagtail_userprofile")
else "system"
)
contrast_name = (
user.wagtail_userprofile.contrast
if hasattr(user, "wagtail_userprofile")
else "system"
)
density_name = (
user.wagtail_userprofile.density
if hasattr(user, "wagtail_userprofile")
else "default"
)
return f"w-theme-{theme_name} w-density-{density_name}"
contrast_name = contrast_name.split("_")[0]
return f"w-theme-{theme_name} w-density-{density_name} w-contrast-{contrast_name}"
@register.simple_tag

Wyświetl plik

@ -232,6 +232,7 @@ class TestAccountSectionUtilsMixin:
"locale-current_time_zone": "Europe/London",
"theme-theme": "dark",
"theme-density": "default",
"theme-contrast": "system",
}
post_data.update(extra_post_data)
return self.client.post(reverse("wagtailadmin_account"), post_data)
@ -480,7 +481,7 @@ class TestAccountSection(WagtailTestUtils, TestCase, TestAccountSectionUtilsMixi
response = self.client.get(reverse("wagtailadmin_home"))
self.assertContains(
response,
'<html lang="es" dir="ltr" class="w-theme-dark w-density-default">',
'<html lang="es" dir="ltr" class="w-theme-dark w-density-default w-contrast-system">',
)
def test_unset_language_preferences(self):
@ -610,6 +611,21 @@ class TestAccountSection(WagtailTestUtils, TestCase, TestAccountSectionUtilsMixi
self.assertEqual(profile.theme, "light")
def test_change_contrast_post(self):
response = self.post_form(
{
"theme-contrast": "more_contrast",
}
)
# Check that the user was redirected to the account page
self.assertRedirects(response, reverse("wagtailadmin_account"))
profile = UserProfile.get_for_user(self.user)
profile.refresh_from_db()
self.assertEqual(profile.contrast, "more_contrast")
def test_change_density_post(self):
response = self.post_form(
{

Wyświetl plik

@ -144,8 +144,8 @@ class TestAuditLogAdmin(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
)
self.assertContains(
response, "system", 3
) # create without a user + remove restriction + 1 from unrelated admin color theme
response, "system", 4
) # create without a user + remove restriction + 2 from unrelated admin color theme
self.assertContains(
response, "the_editor", 9
) # 7 entries by editor + 1 in sidebar menu + 1 in filter

Wyświetl plik

@ -81,7 +81,7 @@ class TestLoginView(WagtailTestUtils, TestCase):
response = self.client.get(reverse("wagtailadmin_login"))
self.assertContains(
response,
'<html lang="de" dir="ltr" class="w-theme-system w-density-default">',
'<html lang="de" dir="ltr" class="w-theme-system w-density-default w-contrast-system">',
)
@override_settings(LANGUAGE_CODE="he")
@ -89,7 +89,7 @@ class TestLoginView(WagtailTestUtils, TestCase):
response = self.client.get(reverse("wagtailadmin_login"))
self.assertContains(
response,
'<html lang="he" dir="rtl" class="w-theme-system w-density-default">',
'<html lang="he" dir="rtl" class="w-theme-system w-density-default w-contrast-system">',
)
@override_settings(

Wyświetl plik

@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-09-23 15:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("wagtailusers", "0013_userprofile_density"),
]
operations = [
migrations.AddField(
model_name="userprofile",
name="contrast",
field=models.CharField(
choices=[("system", "System default"), ("more_contrast", "More contrast")],
default="system",
max_length=40,
verbose_name="contrast",
),
),
]

Wyświetl plik

@ -84,6 +84,17 @@ class UserProfile(models.Model):
max_length=40,
)
class AdminContrastThemes(models.TextChoices):
SYSTEM = "system", _("System default")
MORE_CONTRAST = "more_contrast", _("More contrast")
contrast = models.CharField(
verbose_name=_("contrast"),
choices=AdminContrastThemes.choices,
default=AdminContrastThemes.SYSTEM,
max_length=40,
)
class AdminDensityThemes(models.TextChoices):
DEFAULT = "default", _("Default")
SNUG = "snug", _("Snug")