Rework the Live preview to match D9's actual screenshot

Replaces the earlier sidebar+small-phone mock with D9's real layout: App/
Website/Email tabs (App is the only one with real content, so these are
static labels not a working control), a full-size phone mock, and a public-
site mock, plus the "Where the brand shows up" list. The phone's "Next up"
card now uses primary, not secondary -- it's a notice, not a call to action,
so it shouldn't compete visually with an actual accent-coloured button.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-20 09:19:23 +02:00
parent 59d1fce674
commit 224a3fe4ed
4 changed files with 176 additions and 137 deletions

View File

@@ -115,56 +115,66 @@
<div class="xl:sticky xl:top-4 xl:self-start">
<div class="card">
<div class="card-body">
<div class="card-title">{% lucide "eye" size=16 %} {% trans "Live preview" %}</div>
<p class="text-xs text-muted">{% trans "Where these colours actually show up -- updates as you edit, before you save anything." %}</p>
<div class="mt-2 font-display text-[11px] font-bold tracking-[.12em] text-muted uppercase">{% trans "Management app" %}</div>
<div class="preview-frame mt-1.5">
<div id="preview-sidebar" class="preview-sidebar">
<div class="preview-crest">{{ club.initials|default:"CL" }}</div>
<div class="preview-club-name">{{ club.name|default:_("Your club") }}</div>
<div class="preview-nav-item">{% trans "Overview" %}</div>
<div id="preview-nav-active" class="preview-nav-item preview-nav-active">{% trans "Members" %}</div>
<div class="preview-nav-item">{% trans "Calendar" %}</div>
</div>
<div class="preview-panel">
<div class="preview-panel-heading">{% trans "New member" %}</div>
<button type="button" id="preview-button" class="preview-button">{% trans "Save changes" %}</button>
<div class="preview-skeleton-line"></div>
<div class="preview-skeleton-line preview-skeleton-line-short"></div>
<div class="flex items-center justify-between gap-2">
<div class="card-title">{% lucide "eye" size=16 %} {% trans "Live preview" %}</div>
<div class="flex overflow-hidden rounded-md border border-line">
<span class="preview-tab preview-tab-active">{% trans "App" %}</span>
<span class="preview-tab">{% trans "Website" %}</span>
<span class="preview-tab">{% trans "Email" %}</span>
</div>
</div>
<div class="mt-4 flex items-center justify-between gap-2">
<div class="font-display text-[11px] font-bold tracking-[.12em] text-muted uppercase">{% trans "Mobile app" %}</div>
<span class="badge badge-sm badge-ghost">{% trans "Coming soon" %}</span>
</div>
<div class="mt-1.5 preview-phone">
<div id="preview-phone-header" class="preview-phone-header">
<div class="preview-phone-header-row">
<span class="preview-crest preview-crest-sm">{{ club.initials|default:"CL" }}</span>
<span class="preview-phone-name">{{ club.name|default:_("Your club") }}</span>
<div class="mt-3 flex flex-wrap items-start gap-4">
<div class="preview-phone-lg">
<div id="preview-phone-header" class="preview-phone-header">
<div class="preview-phone-header-row">
<span class="preview-crest preview-crest-sm">{{ club.initials|default:"CL" }}</span>
<span class="preview-phone-name">{{ club.name|default:_("Your club") }}</span>
</div>
<div class="preview-phone-roles">
<span class="preview-phone-role preview-phone-role-active">{% trans "Member" %}</span>
<span class="preview-phone-role">{% trans "Coach" %}</span>
</div>
</div>
<div class="preview-phone-roles">
<span class="preview-phone-role preview-phone-role-active">{% trans "Member" %}</span>
<span class="preview-phone-role">{% trans "Coach" %}</span>
<div class="preview-phone-body">
{# A notice, not a call to action -- primary (chrome), not secondary (accent), so it doesn't visually compete with an actual button. #}
<div id="preview-phone-card" class="preview-phone-card">
<div class="preview-phone-eyebrow">{% trans "Next up" %}</div>
<div class="preview-phone-title">{% trans "U16 vs Leuven" %}</div>
<div class="preview-phone-bar"></div>
</div>
<div class="preview-phone-skeleton-card">
<div class="preview-skeleton-line"></div>
<div class="preview-skeleton-line preview-skeleton-line-short"></div>
</div>
</div>
</div>
<div class="preview-phone-body">
<div id="preview-phone-card" class="preview-phone-card">
<div class="preview-phone-eyebrow">{% trans "Next up" %}</div>
<div class="preview-phone-title">{% trans "U16 vs Leuven" %}</div>
<div class="preview-phone-bar"></div>
</div>
<div class="preview-phone-skeleton-card">
<div class="preview-skeleton-line"></div>
<div class="preview-skeleton-line preview-skeleton-line-short"></div>
</div>
</div>
</div>
<div class="mt-3 text-xs text-dim">
{% trans "Never used for: status colours (paid/overdue, present/absent), table styling, or form fields -- those stay consistent for everyone regardless of club branding." %}
<div class="flex min-w-0 flex-1 flex-col gap-3">
<div class="preview-website">
<div id="preview-website-header" class="preview-website-header">
<span class="preview-crest preview-crest-sm">{{ club.initials|default:"CL" }}</span>
<span class="preview-website-name">{{ club.name|default:_("Your club") }}</span>
<span class="preview-website-nav">{% trans "Teams · Club" %}</span>
<span id="preview-website-join" class="preview-website-join">{% trans "Join" %}</span>
</div>
<div class="preview-website-hero">
<div class="preview-website-headline">{% trans "#Fear the Sharks" %}</div>
<div class="preview-skeleton-line"></div>
<div class="preview-skeleton-line preview-skeleton-line-short"></div>
</div>
</div>
<div class="rounded-lg border border-line p-3">
<div class="mb-2 font-display text-[11px] font-bold tracking-[.12em] text-muted uppercase">{% trans "Where the brand shows up" %}</div>
<ul class="flex flex-col gap-1.5 text-[13px] text-slate">
<li class="flex items-center gap-2"><span id="preview-dot-1" class="preview-brand-dot"></span>{% trans "Sidebar, active nav item, primary buttons" %}</li>
<li class="flex items-center gap-2"><span id="preview-dot-2" class="preview-brand-dot"></span>{% trans "Public site header and join button" %}</li>
<li class="flex items-center gap-2"><span id="preview-dot-3" class="preview-brand-dot"></span>{% trans "Mobile app header and highlights" %}</li>
<li class="flex items-center gap-2"><span class="preview-brand-dot preview-brand-dot-neutral"></span>{% trans "Never: status colours, tables, form fields" %}</li>
</ul>
</div>
</div>
</div>
</div>
</div>
@@ -218,17 +228,21 @@
const primaryContent = contentColorFor(primary);
const secondaryContent = contentColorFor(secondary);
document.getElementById("preview-sidebar").style.background = primary;
document.getElementById("preview-sidebar").style.color = primaryContent;
document.getElementById("preview-nav-active").style.background = secondary;
document.getElementById("preview-nav-active").style.color = secondaryContent;
document.getElementById("preview-button").style.background = secondary;
document.getElementById("preview-button").style.color = secondaryContent;
document.getElementById("preview-phone-header").style.background = primary;
document.getElementById("preview-phone-header").style.color = primaryContent;
document.getElementById("preview-phone-card").style.background = secondary;
document.getElementById("preview-phone-card").style.color = secondaryContent;
// A notice, not a call to action -- primary (chrome), not secondary
// (accent), so it doesn't compete for attention with an actual button.
document.getElementById("preview-phone-card").style.background = primary;
document.getElementById("preview-phone-card").style.color = primaryContent;
document.getElementById("preview-website-header").style.background = primary;
document.getElementById("preview-website-header").style.color = primaryContent;
document.getElementById("preview-website-join").style.background = secondary;
document.getElementById("preview-website-join").style.color = secondaryContent;
["preview-dot-1", "preview-dot-2", "preview-dot-3"].forEach((id) => {
document.getElementById(id).style.background = secondary;
});
setContrastBox("contrast-box-primary", "contrast-label-primary", "contrast-ratio-primary", primary, primaryContent, "{% trans 'on primary' %}");
setContrastBox("contrast-box-secondary", "contrast-label-secondary", "contrast-ratio-secondary", secondary, secondaryContent, "{% trans 'on secondary' %}");

View File

@@ -4465,18 +4465,19 @@ class ClubSettingsPreviewTests(ManagementTestBase):
def setUp(self):
self.client.force_login(self.admin_user)
def test_the_preview_frame_and_colour_swatches_render(self):
def test_the_preview_tabs_and_colour_swatches_render(self):
response = self.club_get("club_settings")
self.assertContains(response, "preview-sidebar")
self.assertContains(response, "preview-tab-active")
self.assertContains(response, 'id="primary_color_swatch"')
self.assertContains(response, 'id="secondary_color_swatch"')
def test_the_mobile_app_preview_renders(self):
def test_the_mobile_app_and_website_previews_render(self):
response = self.club_get("club_settings")
self.assertContains(response, "preview-phone")
self.assertContains(response, "Coming soon")
self.assertContains(response, "preview-phone-lg")
self.assertContains(response, "preview-website")
self.assertContains(response, "Where the brand shows up")
def test_the_website_field_is_editable(self):
response = self.club_get("club_settings")