Merge the identity page's App/Website preview tabs and make Email functional
App and Website were always shown together already, so giving them separate tabs implied a toggle that didn't exist -- merged into one "App & Website" tab. The Email tab now shows a real, colour-bound sample (plain header with a secondary-coloured crest badge, a primary-coloured button) matching the actual branded emails this app already sends -- audited every mail-sending call site (claims.py, the new invoicing service, and platform billing's club-facing reminders) and the first two already use the branded shell; billing's is intentionally unbranded since it's RosterChief invoicing the club, not the club invoicing its own members. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
@@ -1804,9 +1804,10 @@
|
|||||||
background: var(--color-paper);
|
background: var(--color-paper);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Live preview tabs -- App is the only one with real content behind it
|
/* Live preview tabs -- a real (if minimal) tab control: click toggles which
|
||||||
(Website/Email aren't built), so these are static labels, not a real
|
.preview-panel is shown (club_settings.html's own inline script). App and
|
||||||
tab control. */
|
Website share one tab since the mobile app isn't built yet and the two
|
||||||
|
mocks were already always shown together underneath it. */
|
||||||
.preview-tab {
|
.preview-tab {
|
||||||
padding: 0.3125rem 0.625rem;
|
padding: 0.3125rem 0.625rem;
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
@@ -1815,6 +1816,7 @@
|
|||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--color-muted);
|
color: var(--color-muted);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-tab-active {
|
.preview-tab-active {
|
||||||
@@ -2045,4 +2047,58 @@
|
|||||||
.preview-brand-dot-neutral {
|
.preview-brand-dot-neutral {
|
||||||
background: var(--color-dim);
|
background: var(--color-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The Email tab -- a generic sample, not any one real message. Shaped like
|
||||||
|
templates/email/_base.html's actual output (plain white header with a
|
||||||
|
coloured crest badge, a primary-coloured button in the body) rather than
|
||||||
|
the phone/website mocks' solid colour bar: that's genuinely how every
|
||||||
|
branded email (club/templates/club/email/*.html, members/.../claim_approved.html)
|
||||||
|
looks, so this preview is accurate, not another approximation. */
|
||||||
|
.preview-email {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--color-line);
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-email-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid var(--color-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-email-crest {
|
||||||
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-email-name {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-email-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.875rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-email-button {
|
||||||
|
align-self: flex-start;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.3125rem 0.75rem;
|
||||||
|
margin: 0.125rem 0;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,16 @@
|
|||||||
closely for the Colours card (merged swatch+hex row, side-by-side
|
closely for the Colours card (merged swatch+hex row, side-by-side
|
||||||
contrast-check boxes) and the Logo card (dashed drop zone) -- markup
|
contrast-check boxes) and the Logo card (dashed drop zone) -- markup
|
||||||
pulled straight from the design canvas (RosterChief Platform.dc.html),
|
pulled straight from the design canvas (RosterChief Platform.dc.html),
|
||||||
not the prose summary. The live preview is scoped to surfaces that are
|
not the prose summary. The live preview has two tabs rather than D9's
|
||||||
(or will genuinely be) this app rather than D9's aspirational App/
|
three: App and Website are merged into one (they were always shown
|
||||||
Website/Email tabs: the sidebar mock is what primary_color/secondary_color
|
together anyway, and the mobile app isn't built yet -- the phone mock
|
||||||
affect today (base.html's --tenant-* custom properties); the phone mock
|
is the same colours applied ahead of it existing); Email is real and
|
||||||
is the same colours applied ahead of the mobile app being built. The
|
functional, a generic sample shaped like the actual branded emails this
|
||||||
colour math (content colour, contrast ratio) mirrors
|
app sends (club/templates/club/email/*.html, members/.../claim_approved.html),
|
||||||
Club._content_color_for exactly, so what's shown is the real number the
|
not a preview of any one specific message. The colour math (content
|
||||||
server computes on save, not an approximation.
|
colour, contrast ratio) mirrors Club._content_color_for exactly, so
|
||||||
|
what's shown is the real number the server computes on save, not an
|
||||||
|
approximation.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% block panel_title %}{% trans "Club identity" %}{% endblock panel_title %}
|
{% block panel_title %}{% trans "Club identity" %}{% endblock panel_title %}
|
||||||
@@ -118,13 +120,13 @@
|
|||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<div class="card-title">{% lucide "eye" size=16 %} {% trans "Live preview" %}</div>
|
<div class="card-title">{% lucide "eye" size=16 %} {% trans "Live preview" %}</div>
|
||||||
<div class="flex overflow-hidden rounded-md border border-line">
|
<div class="flex overflow-hidden rounded-md border border-line">
|
||||||
<span class="preview-tab preview-tab-active">{% trans "App" %}</span>
|
<button type="button" class="preview-tab preview-tab-active" data-preview-tab="app">{% trans "App & Website" %}</button>
|
||||||
<span class="preview-tab">{% trans "Website" %}</span>
|
<button type="button" class="preview-tab" data-preview-tab="email">{% trans "Email" %}</button>
|
||||||
<span class="preview-tab">{% trans "Email" %}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 flex flex-wrap items-start gap-4">
|
<div class="mt-3">
|
||||||
|
<div id="preview-panel-app" class="flex flex-wrap items-start gap-4" data-preview-panel="app">
|
||||||
<div class="preview-phone-lg">
|
<div class="preview-phone-lg">
|
||||||
<div id="preview-phone-header" class="preview-phone-header">
|
<div id="preview-phone-header" class="preview-phone-header">
|
||||||
<div class="preview-phone-header-row">
|
<div class="preview-phone-header-row">
|
||||||
@@ -171,11 +173,26 @@
|
|||||||
<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-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-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 id="preview-dot-3" class="preview-brand-dot"></span>{% trans "Mobile app header and highlights" %}</li>
|
||||||
|
<li class="flex items-center gap-2"><span id="preview-dot-4" class="preview-brand-dot"></span>{% trans "Transactional email crest and buttons" %}</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>
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="preview-panel-email" class="preview-email hidden" data-preview-panel="email">
|
||||||
|
<div class="preview-email-header">
|
||||||
|
<span id="preview-email-crest" class="preview-crest preview-crest-sm preview-email-crest">{{ club.initials|default:"CL" }}</span>
|
||||||
|
<span class="preview-email-name">{{ club.name|default:_("Your club") }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-email-body">
|
||||||
|
<div class="preview-skeleton-line"></div>
|
||||||
|
<div class="preview-skeleton-line preview-skeleton-line-short"></div>
|
||||||
|
<span id="preview-email-button" class="preview-email-button">{% trans "View details" %}</span>
|
||||||
|
<div class="preview-skeleton-line preview-skeleton-line-short"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -240,7 +257,16 @@
|
|||||||
document.getElementById("preview-website-join").style.background = secondary;
|
document.getElementById("preview-website-join").style.background = secondary;
|
||||||
document.getElementById("preview-website-join").style.color = secondaryContent;
|
document.getElementById("preview-website-join").style.color = secondaryContent;
|
||||||
|
|
||||||
["preview-dot-1", "preview-dot-2", "preview-dot-3"].forEach((id) => {
|
// Matches the real branded emails (club/templates/club/email/*.html):
|
||||||
|
// a plain white header with a secondary-coloured crest badge, and a
|
||||||
|
// primary-coloured button in the body -- same split as _button.html's
|
||||||
|
// own bg=primary convention, not the phone/website mocks' solid bar.
|
||||||
|
document.getElementById("preview-email-crest").style.background = secondary;
|
||||||
|
document.getElementById("preview-email-crest").style.color = secondaryContent;
|
||||||
|
document.getElementById("preview-email-button").style.background = primary;
|
||||||
|
document.getElementById("preview-email-button").style.color = primaryContent;
|
||||||
|
|
||||||
|
["preview-dot-1", "preview-dot-2", "preview-dot-3", "preview-dot-4"].forEach((id) => {
|
||||||
document.getElementById(id).style.background = secondary;
|
document.getElementById(id).style.background = secondary;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -267,6 +293,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
updatePreview();
|
updatePreview();
|
||||||
|
|
||||||
|
// Live preview tabs -- toggles which .preview-panel is visible, App &
|
||||||
|
// Website vs Email. Both panels stay in the DOM (and colour-bound) the
|
||||||
|
// whole time; only which one is hidden changes.
|
||||||
|
document.querySelectorAll("[data-preview-tab]").forEach((tab) => {
|
||||||
|
tab.addEventListener("click", () => {
|
||||||
|
document.querySelectorAll("[data-preview-tab]").forEach((other) => other.classList.toggle("preview-tab-active", other === tab));
|
||||||
|
document.querySelectorAll("[data-preview-panel]").forEach((panel) => panel.classList.toggle("hidden", panel.dataset.previewPanel !== tab.dataset.previewTab));
|
||||||
|
});
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endblock extra_body %}
|
{% endblock extra_body %}
|
||||||
|
|||||||
@@ -4686,6 +4686,19 @@ class ClubSettingsPreviewTests(ManagementTestBase):
|
|||||||
response = self.club_get("club_settings")
|
response = self.club_get("club_settings")
|
||||||
self.assertContains(response, 'name="website"')
|
self.assertContains(response, 'name="website"')
|
||||||
|
|
||||||
|
def test_app_and_website_share_one_tab(self):
|
||||||
|
response = self.club_get("club_settings")
|
||||||
|
|
||||||
|
self.assertContains(response, "App & Website")
|
||||||
|
self.assertContains(response, 'data-preview-tab="app"')
|
||||||
|
self.assertNotContains(response, 'data-preview-tab="website"')
|
||||||
|
|
||||||
|
def test_the_email_tab_renders_and_starts_hidden(self):
|
||||||
|
response = self.club_get("club_settings")
|
||||||
|
|
||||||
|
self.assertContains(response, 'data-preview-tab="email"')
|
||||||
|
self.assertContains(response, 'id="preview-panel-email" class="preview-email hidden"')
|
||||||
|
|
||||||
def test_saving_still_updates_the_club(self):
|
def test_saving_still_updates_the_club(self):
|
||||||
response = self.club_post(
|
response = self.club_post(
|
||||||
"club_settings",
|
"club_settings",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user