Architektur: - shared/theme.js — Logik (data-theme attr auf <html>, localStorage, prefers-color-scheme fallback, data-theme-lock opt-out) - shared/toggles.js — fixed top-right Pill mit Sun/Moon SVG + DE/EN Button (auto-injected, hängt sich an i18n.js's [data-i18n-toggle] Pattern) - shared/css/theme.css — neutrale Light-Defaults (cream bg, AA-konforme grays) - templates/base.html — Anti-FOUC inline IIFE im <head>, theme.css linked vor inline <style>, scripts in body - tools/contrast-audit.py — neue --light/--dark/--both Modi, parsed [data-theme="light"] + shared fallback Pilot auf 4 Sites: - ichbinotto.de (Octopus rot/teal) - paragraphenraiter.de (Gold) - kilitaer.de (Olive) - deinesei.de (Indigo) Audit-Ergebnis: - Dark mode: 0/59 Verstöße (regression-frei) - Light mode: 14/59 Sites brauchen per-site overrides für sub-AA Akzent-Vars (Shift-2 follow-up) Out of Scope (Shift-2): - Rollout auf restliche 55 Sites - Per-Site Light-Palette-Verfeinerung wo neutral-Default nicht trägt - Per-Site Opt-Out (data-theme-lock) für aesthetisch dark-only Satire-Sites Design-Doc: docs/plans/theme-toggle.md
133 lines
5.1 KiB
JavaScript
133 lines
5.1 KiB
JavaScript
/**
|
|
* Combined Theme + Language toggle widget for onepager sites.
|
|
*
|
|
* Auto-injects a fixed top-right pill with two buttons:
|
|
* [☀/🌙] [DE/EN]
|
|
*
|
|
* Depends on theme.js (window.onepagerTheme) and i18n.js ([data-i18n-toggle]).
|
|
* Include all three in this order at end of <body>:
|
|
* <script src="/shared/theme.js"></script>
|
|
* <script src="/shared/i18n.js"></script>
|
|
* <script src="/shared/toggles.js"></script>
|
|
*
|
|
* Per-site opt-out:
|
|
* <html data-no-toggles> — skip both buttons
|
|
* <html data-theme-lock="dark"> — hide theme button, keep lang
|
|
* <script data-no-lang> — on toggles.js tag, hide lang button
|
|
*/
|
|
(function () {
|
|
if (document.documentElement.hasAttribute('data-no-toggles')) return;
|
|
|
|
var script = document.currentScript;
|
|
var hideLang = script && script.hasAttribute('data-no-lang');
|
|
var hideTheme = !window.onepagerTheme || window.onepagerTheme.isLocked();
|
|
|
|
if (hideTheme && hideLang) return;
|
|
|
|
var SUN = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>';
|
|
var MOON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
|
|
|
|
function inject() {
|
|
if (document.getElementById('onepager-toggles')) return;
|
|
|
|
var pill = document.createElement('div');
|
|
pill.id = 'onepager-toggles';
|
|
pill.style.cssText = [
|
|
'position:fixed',
|
|
'top:12px',
|
|
'right:12px',
|
|
'z-index:9999',
|
|
'display:flex',
|
|
'gap:0',
|
|
'align-items:stretch',
|
|
'border-radius:999px',
|
|
'padding:3px',
|
|
'background:var(--bg-card,rgba(255,255,255,0.6))',
|
|
'border:1px solid var(--border,rgba(127,127,127,0.25))',
|
|
'backdrop-filter:blur(8px)',
|
|
'-webkit-backdrop-filter:blur(8px)',
|
|
'box-shadow:0 2px 8px rgba(0,0,0,0.08)',
|
|
'font-family:inherit'
|
|
].join(';');
|
|
|
|
var btnStyle = [
|
|
'background:transparent',
|
|
'border:0',
|
|
'color:var(--text,inherit)',
|
|
'cursor:pointer',
|
|
'padding:6px 10px',
|
|
'font-size:0.7rem',
|
|
'font-weight:500',
|
|
'letter-spacing:0.05em',
|
|
'border-radius:999px',
|
|
'display:inline-flex',
|
|
'align-items:center',
|
|
'justify-content:center',
|
|
'min-width:34px',
|
|
'min-height:28px',
|
|
'line-height:1',
|
|
'transition:background 0.15s,color 0.15s',
|
|
'opacity:0.85'
|
|
].join(';');
|
|
|
|
if (!hideTheme) {
|
|
var themeBtn = document.createElement('button');
|
|
themeBtn.type = 'button';
|
|
themeBtn.id = 'onepager-theme-toggle';
|
|
themeBtn.style.cssText = btnStyle;
|
|
themeBtn.setAttribute('aria-label', 'Toggle light/dark theme');
|
|
themeBtn.title = 'Hell/Dunkel umschalten';
|
|
function renderTheme() {
|
|
var isLight = window.onepagerTheme && window.onepagerTheme.get() === 'light';
|
|
themeBtn.innerHTML = isLight ? MOON : SUN;
|
|
}
|
|
renderTheme();
|
|
themeBtn.addEventListener('click', function () {
|
|
if (window.onepagerTheme) window.onepagerTheme.toggle();
|
|
renderTheme();
|
|
});
|
|
themeBtn.addEventListener('mouseenter', function () { themeBtn.style.opacity = '1'; });
|
|
themeBtn.addEventListener('mouseleave', function () { themeBtn.style.opacity = '0.85'; });
|
|
pill.appendChild(themeBtn);
|
|
}
|
|
|
|
if (!hideLang) {
|
|
if (!hideTheme) {
|
|
var sep = document.createElement('span');
|
|
sep.style.cssText = 'width:1px;background:var(--border,rgba(127,127,127,0.25));margin:4px 0;';
|
|
sep.setAttribute('aria-hidden', 'true');
|
|
pill.appendChild(sep);
|
|
}
|
|
var langBtn = document.createElement('button');
|
|
langBtn.type = 'button';
|
|
langBtn.id = 'onepager-lang-toggle';
|
|
langBtn.setAttribute('data-i18n-toggle', '');
|
|
langBtn.style.cssText = btnStyle;
|
|
langBtn.title = 'Maschinell übersetzt / Machine-translated — German is the original.';
|
|
// i18n.js fills text & aria-label and binds click; placeholder until then:
|
|
langBtn.textContent = (document.documentElement.lang || 'de') === 'de' ? 'EN' : 'DE';
|
|
langBtn.addEventListener('mouseenter', function () { langBtn.style.opacity = '1'; });
|
|
langBtn.addEventListener('mouseleave', function () { langBtn.style.opacity = '0.85'; });
|
|
pill.appendChild(langBtn);
|
|
}
|
|
|
|
document.body.appendChild(pill);
|
|
|
|
// If i18n.js already initialised (loaded before us), it won't have bound
|
|
// the new button — re-bind manually.
|
|
if (!hideLang && window.onepagerI18n) {
|
|
var lb = document.getElementById('onepager-lang-toggle');
|
|
if (lb) {
|
|
lb.addEventListener('click', window.onepagerI18n.toggle);
|
|
lb.textContent = (document.documentElement.lang || 'de') === 'de' ? 'EN' : 'DE';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', inject);
|
|
} else {
|
|
inject();
|
|
}
|
|
})();
|