chore: polish cookie banner — animation, UX, accessibility

- Slide-up entrance / slide-down exit animations (CSS keyframes)
- Upward box-shadow for visual depth from page content
- Larger toggle (44×24px) for easier tap target on mobile
- Split dual-state Manage/Save into: header button Manage↔Close + panel Save choices button
- ARIA: role=dialog, role=switch, aria-checked on toggle, focus moves to prefs on open
- Use btn-outline / btn component classes for secondary/primary buttons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-20 02:06:38 +01:00
parent a7b38339a6
commit 8671facd7c

View File

@@ -1,47 +1,99 @@
{# Cookie consent banner — included in base.html before </body>. {# Cookie consent banner — included in base.html before </body>.
JS-driven: hidden by default, shown only if cookie_consent cookie is absent. JS-driven: hidden by default, shown only if cookie_consent cookie is absent.
No-JS behaviour: banner stays hidden → only essential cookies are set (privacy-safe default). #} No-JS behaviour: banner stays hidden → only essential cookies set (privacy-safe default). #}
<div id="cookie-banner" style="display:none;position:fixed;bottom:0;left:0;right:0;z-index:200;background:#fff;border-top:1px solid #E2E8F0"> <style>
<div class="container-page" style="padding-top:0.875rem;padding-bottom:0.875rem"> @keyframes cb-slide-up { from { transform:translateY(100%); opacity:0 } to { transform:translateY(0); opacity:1 } }
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;flex-wrap:wrap"> @keyframes cb-slide-down { from { transform:translateY(0); opacity:1 } to { transform:translateY(100%); opacity:0 } }
<div style="flex:1;min-width:260px"> #cookie-banner.cb-enter { animation: cb-slide-up 0.35s cubic-bezier(0.22,1,0.36,1) forwards }
<p style="font-size:0.8125rem;color:#475569;margin:0 0 4px"> #cookie-banner.cb-exit { animation: cb-slide-down 0.25s ease-in forwards }
We use cookies for essential site functions. #cookie-manage-btn:hover { background:#F1F5F9!important; border-color:#CBD5E1!important }
<a href="{{ url_for('public.privacy') }}#cookies" style="text-decoration:underline;color:#1D4ED8">Learn more</a> #cookie-save-btn:hover { border-color:#1D4ED8!important; color:#1D4ED8!important }
#cookie-functional-label:focus-within #cookie-track { outline:2px solid #1D4ED8; outline-offset:2px }
</style>
<div id="cookie-banner"
role="dialog" aria-label="Cookie consent"
style="display:none;position:fixed;bottom:0;left:0;right:0;z-index:200;
background:#fff;border-top:1px solid #E2E8F0;
box-shadow:0 -4px 32px rgba(15,23,42,0.09)">
<div class="container-page" style="padding-top:1rem;padding-bottom:1rem">
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:1.25rem;flex-wrap:wrap">
<!-- Left: copy + collapsible prefs -->
<div style="flex:1;min-width:240px">
<p style="font-size:0.8125rem;color:#475569;margin:0;line-height:1.55">
We use cookies to keep you signed in and improve the site.
<a href="{{ url_for('public.privacy') }}#cookies"
style="color:#1D4ED8;text-decoration:underline;text-underline-offset:2px">Cookie policy</a>
</p> </p>
<!-- Preferences panel, collapsed by default -->
<!-- Preferences panel (collapsed by default) -->
<div id="cookie-prefs" hidden <div id="cookie-prefs" hidden
style="margin-top:0.75rem;padding:0.75rem;border:1px solid #E2E8F0;border-radius:10px;background:#F8FAFC"> style="margin-top:0.875rem;padding:0.875rem;
<!-- Essential row (locked) --> border:1px solid #E2E8F0;border-radius:12px;background:#F8FAFC">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:2px">
<span style="font-size:0.8125rem;font-weight:600;color:#1E293B">Essential</span> <!-- Essential row -->
<span style="font-size:0.75rem;color:#16A34A;font-weight:600">Always on</span> <div style="display:flex;justify-content:space-between;align-items:flex-start;
padding-bottom:0.75rem;margin-bottom:0.75rem;border-bottom:1px solid #F1F5F9">
<div>
<div style="font-size:0.8125rem;font-weight:600;color:#0F172A;margin-bottom:2px">Essential</div>
<div style="font-size:0.75rem;color:#64748B;line-height:1.4">Session management. Always required.</div>
</div>
<span style="font-size:0.625rem;font-weight:700;letter-spacing:0.06em;color:#16A34A;
background:#F0FDF4;border:1px solid #BBF7D0;border-radius:6px;
padding:3px 8px;flex-shrink:0;margin-top:2px">ON</span>
</div> </div>
<p style="font-size:0.75rem;color:#64748B;margin:0 0 10px">Session management. Cannot be disabled.</p>
<!-- Functional row (toggle) --> <!-- Functional row -->
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:2px"> <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:0.75rem">
<span style="font-size:0.8125rem;font-weight:600;color:#1E293B">Functional</span> <div style="flex:1">
<div style="font-size:0.8125rem;font-weight:600;color:#0F172A;margin-bottom:2px">Functional</div>
<div style="font-size:0.75rem;color:#64748B;line-height:1.4">A/B testing to improve the experience.</div>
</div>
<!-- Toggle switch --> <!-- Toggle switch -->
<label style="position:relative;display:inline-block;width:36px;height:20px;cursor:pointer;flex-shrink:0"> <label id="cookie-functional-label"
<input type="checkbox" id="cookie-functional" style="opacity:0;width:0;height:0;position:absolute"> style="position:relative;display:inline-block;width:44px;height:24px;
<span id="cookie-track" style="position:absolute;inset:0;background:#CBD5E1;border-radius:20px;transition:background 0.2s"></span> cursor:pointer;flex-shrink:0;margin-top:1px">
<span id="cookie-dot" style="position:absolute;top:2px;left:2px;width:16px;height:16px;background:#fff;border-radius:50%;transition:transform 0.2s;box-shadow:0 1px 3px rgba(0,0,0,.2)"></span> <input type="checkbox" id="cookie-functional"
role="switch" aria-checked="false" aria-label="Functional cookies"
style="opacity:0;width:0;height:0;position:absolute">
<span id="cookie-track"
style="position:absolute;inset:0;background:#CBD5E1;border-radius:24px;
transition:background 0.2s ease;border-radius:99px"></span>
<span id="cookie-dot"
style="position:absolute;top:3px;left:3px;width:18px;height:18px;
background:#fff;border-radius:50%;
box-shadow:0 1px 4px rgba(0,0,0,0.22);
transition:transform 0.2s ease"></span>
</label> </label>
</div> </div>
<p style="font-size:0.75rem;color:#64748B;margin:0">A/B testing to improve the site experience.</p>
<!-- Save choices -->
<div style="margin-top:0.875rem;display:flex;justify-content:flex-end">
<button type="button" id="cookie-save-btn"
style="font-size:0.8125rem;font-family:inherit;cursor:pointer;
padding:6px 16px;border:1px solid #E2E8F0;border-radius:8px;
background:white;color:#475569;
transition:border-color 0.15s ease,color 0.15s ease">
Save choices
</button>
</div>
</div> </div>
</div> </div>
<!-- Action buttons -->
<div style="display:flex;gap:8px;flex-shrink:0;align-items:flex-start;padding-top:2px"> <!-- Right: action buttons -->
<button type="button" id="cookie-manage-btn" <div style="display:flex;gap:8px;flex-shrink:0;align-items:flex-start;padding-top:1px">
style="font-size:0.8125rem;padding:7px 14px;border:1px solid #E2E8F0;border-radius:10px;background:white;cursor:pointer;color:#475569;font-family:inherit;white-space:nowrap"> <button type="button" id="cookie-manage-btn" class="btn-outline btn-sm"
Manage preferences style="font-size:0.8125rem;white-space:nowrap;padding:8px 16px;
border-radius:10px;transition:background 0.15s,border-color 0.15s">
Manage
</button> </button>
<button type="button" id="cookie-accept-btn" class="btn" <button type="button" id="cookie-accept-btn" class="btn btn-sm"
style="font-size:0.8125rem;padding:7px 14px;white-space:nowrap"> style="font-size:0.8125rem;white-space:nowrap;padding:8px 18px;border-radius:10px">
Accept all Accept all
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -49,48 +101,57 @@
<script> <script>
(function () { (function () {
var COOKIE_NAME = 'cookie_consent'; var COOKIE_NAME = 'cookie_consent';
var MAX_AGE = 365 * 24 * 60 * 60; var MAX_AGE = 365 * 24 * 60 * 60;
var banner = document.getElementById('cookie-banner'); var banner = document.getElementById('cookie-banner');
// Show only if no consent cookie exists yet // Already consented — bail out
if (document.cookie.split(';').some(function (c) { if (document.cookie.split(';').some(function (c) {
return c.trim().startsWith(COOKIE_NAME + '='); return c.trim().startsWith(COOKIE_NAME + '=');
})) return; })) return;
banner.style.display = 'block';
var prefs = document.getElementById('cookie-prefs'); // Show with slide-up animation
var checkbox = document.getElementById('cookie-functional'); banner.style.display = 'block';
var track = document.getElementById('cookie-track'); requestAnimationFrame(function () { banner.classList.add('cb-enter'); });
var dot = document.getElementById('cookie-dot');
var prefs = document.getElementById('cookie-prefs');
var checkbox = document.getElementById('cookie-functional');
var track = document.getElementById('cookie-track');
var dot = document.getElementById('cookie-dot');
var manageBtn = document.getElementById('cookie-manage-btn'); var manageBtn = document.getElementById('cookie-manage-btn');
var saveBtn = document.getElementById('cookie-save-btn');
var acceptBtn = document.getElementById('cookie-accept-btn'); var acceptBtn = document.getElementById('cookie-accept-btn');
var panelOpen = false;
function updateToggle() { function updateToggle() {
track.style.background = checkbox.checked ? '#1D4ED8' : '#CBD5E1'; var on = checkbox.checked;
dot.style.transform = checkbox.checked ? 'translateX(16px)' : 'translateX(0)'; track.style.background = on ? '#1D4ED8' : '#CBD5E1';
dot.style.transform = on ? 'translateX(20px)' : 'translateX(0)';
checkbox.setAttribute('aria-checked', on ? 'true' : 'false');
} }
checkbox.addEventListener('change', updateToggle); checkbox.addEventListener('change', updateToggle);
manageBtn.addEventListener('click', function () { manageBtn.addEventListener('click', function () {
if (!panelOpen) { var opening = prefs.hidden;
prefs.hidden = false; prefs.hidden = !opening;
manageBtn.textContent = 'Save preferences'; manageBtn.textContent = opening ? 'Close' : 'Manage';
panelOpen = true; if (opening) prefs.querySelector('input').focus();
} else { });
var cats = ['essential'];
if (checkbox.checked) cats.push('functional'); saveBtn.addEventListener('click', function () {
setConsent(cats.join(',')); var cats = ['essential'];
} if (checkbox.checked) cats.push('functional');
dismiss(cats.join(','));
}); });
acceptBtn.addEventListener('click', function () { acceptBtn.addEventListener('click', function () {
setConsent('essential,functional'); dismiss('essential,functional');
}); });
function setConsent(value) { function dismiss(value) {
document.cookie = COOKIE_NAME + '=' + value + ';path=/;max-age=' + MAX_AGE + ';SameSite=Lax'; document.cookie = COOKIE_NAME + '=' + value
banner.style.display = 'none'; + ';path=/;max-age=' + MAX_AGE + ';SameSite=Lax';
banner.classList.remove('cb-enter');
banner.classList.add('cb-exit');
setTimeout(function () { banner.style.display = 'none'; }, 280);
} }
}()); }());
</script> </script>