Add a secondary brand colour for clubs
Mirrors primary_color: a club-picked hex highlight used for accents like the avatar-initials badge, with a computed readable text colour so a pale pick doesn't produce white-on-yellow text. The shared contrast math moves into _content_color_for so both colours use the same rule.
This commit is contained in:
19
club/migrations/0014_club_secondary_color.py
Normal file
19
club/migrations/0014_club_secondary_color.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 6.0.6 on 2026-07-24 16:30
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('club', '0013_club_created_club_modified_clubmembership_created_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='club',
|
||||
name='secondary_color',
|
||||
field=models.CharField(blank=True, help_text="Hex colour for highlights on the club's pages, e.g. avatar initials. Defaults to the theme's secondary colour.", max_length=7, validators=[django.core.validators.RegexValidator('^#[0-9a-fA-F]{6}$', 'Enter a colour as a hex value, e.g. #be185d.')], verbose_name='secondary colour'),
|
||||
),
|
||||
]
|
||||
@@ -40,6 +40,14 @@ class Club(UUIDModel):
|
||||
help_text=_("Hex colour for buttons and links on the club's pages, e.g. #1e40af."),
|
||||
)
|
||||
|
||||
secondary_color = models.CharField(
|
||||
_("secondary colour"),
|
||||
max_length=7,
|
||||
blank=True,
|
||||
validators=[RegexValidator(r"^#[0-9a-fA-F]{6}$", _("Enter a colour as a hex value, e.g. #be185d."))],
|
||||
help_text=_("Hex colour for highlights on the club's pages, e.g. avatar initials. Defaults to the theme's secondary colour."),
|
||||
)
|
||||
|
||||
archived_at = models.DateTimeField(_("archived at"), null=True, blank=True, help_text=_("Archived clubs stop resolving on their subdomain, but their data is retained."))
|
||||
|
||||
objects = ClubManager()
|
||||
@@ -69,19 +77,29 @@ class Club(UUIDModel):
|
||||
|
||||
@property
|
||||
def primary_content_color(self) -> str:
|
||||
"""Readable text colour to sit *on* ``primary_color``.
|
||||
"""Readable text colour to sit *on* ``primary_color``. See ``_content_color_for``."""
|
||||
return self._content_color_for(self.primary_color)
|
||||
|
||||
@property
|
||||
def secondary_content_color(self) -> str:
|
||||
"""Readable text colour to sit *on* ``secondary_color``. See ``_content_color_for``."""
|
||||
return self._content_color_for(self.secondary_color)
|
||||
|
||||
@staticmethod
|
||||
def _content_color_for(hex_color: str) -> str:
|
||||
"""Black or white, whichever reads on ``hex_color``.
|
||||
|
||||
A club picking a pale yellow would otherwise get white-on-yellow buttons.
|
||||
Relative luminance per WCAG, with its 0.179 threshold for black vs white.
|
||||
"""
|
||||
if not self.primary_color:
|
||||
if not hex_color:
|
||||
return ""
|
||||
|
||||
def channel(value: int) -> float:
|
||||
fraction = value / 255
|
||||
return fraction / 12.92 if fraction <= 0.04045 else ((fraction + 0.055) / 1.055) ** 2.4
|
||||
|
||||
red, green, blue = (channel(int(self.primary_color[index : index + 2], 16)) for index in (1, 3, 5))
|
||||
red, green, blue = (channel(int(hex_color[index : index + 2], 16)) for index in (1, 3, 5))
|
||||
luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue
|
||||
|
||||
return "#000000" if luminance > 0.179 else "#ffffff"
|
||||
|
||||
@@ -1018,6 +1018,15 @@ class BrandingTests(TestCase):
|
||||
def test_no_colour_means_no_override(self):
|
||||
self.assertNotContains(self.login_page("ajax-united.rosterchief.app"), "--color-primary")
|
||||
|
||||
def test_a_club_secondary_colour_overrides_the_theme(self):
|
||||
self.club.secondary_color = "#be185d"
|
||||
self.club.save()
|
||||
|
||||
self.assertContains(self.login_page("ajax-united.rosterchief.app"), "--color-secondary: #be185d")
|
||||
|
||||
def test_no_secondary_colour_means_no_override(self):
|
||||
self.assertNotContains(self.login_page("ajax-united.rosterchief.app"), "--color-secondary")
|
||||
|
||||
|
||||
class ClubBrandingModelTests(TestCase):
|
||||
def test_initials_use_the_first_two_words(self):
|
||||
|
||||
Reference in New Issue
Block a user