Added color switching, moved scripts into dedicated files

This commit is contained in:
2026-08-13 14:59:41 +02:00
parent 1bd70cb16b
commit 828c7dc3b6
6 changed files with 371 additions and 94 deletions
+123 -24
View File
@@ -1,4 +1,4 @@
@import url("pygments.css"); @import url("pygments.css");
@import url("pygments-dark.css") (prefers-color-theme: dark); @import url("pygments-dark.css") (prefers-color-theme: dark);
@font-face { @font-face {
@@ -44,9 +44,16 @@
:root { :root {
--main-bg-color: hsl(210, 20%, 98%); --main-bg-color: hsl(210, 20%, 98%);
--main-text-color: hsl(0, 5%, 10%); --main-text-color: hsl(0, 5%, 10%);
--main-link-color: hsl(0, 100%, 35%); --main-link-color: hsl(220, 90%, 45%);
--accent-default: hsl(220, 90%, 45%);
--accent-alt: hsl(140, 45%, 28%);
--table-border-color: hsl(0, 0%, 85%); --table-border-color: hsl(0, 0%, 85%);
--table-head-border-color: hsl(0, 0%, 70%); --table-head-border-color: hsl(0, 0%, 70%);
--main-font: 'Latin Modern', Georgia, Cambria, 'Times New Roman', Times, serif;
}
html[data-font="sans"] {
--main-font: 'Segoe UI', system-ui, -apple-system, Roboto, Helvetica, Arial, sans-serif;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@@ -54,6 +61,8 @@
--main-bg-color: hsl(0, 5%, 10%); --main-bg-color: hsl(0, 5%, 10%);
--main-text-color: hsl(210, 20%, 98%); --main-text-color: hsl(210, 20%, 98%);
--main-link-color: hsl(0, 100%, 65%); --main-link-color: hsl(0, 100%, 65%);
--accent-default: hsl(0, 100%, 65%);
--accent-alt: hsl(55, 100%, 60%);
--table-border-color: hsl(0, 0%, 15%); --table-border-color: hsl(0, 0%, 15%);
--table-head-border-color: hsl(0, 0%, 30%); --table-head-border-color: hsl(0, 0%, 30%);
} }
@@ -63,6 +72,8 @@
--main-bg-color: hsl(0, 5%, 10%); --main-bg-color: hsl(0, 5%, 10%);
--main-text-color: hsl(210, 20%, 98%); --main-text-color: hsl(210, 20%, 98%);
--main-link-color: hsl(0, 100%, 65%); --main-link-color: hsl(0, 100%, 65%);
--accent-default: hsl(0, 100%, 65%);
--accent-alt: hsl(55, 100%, 60%);
--table-border-color: hsl(0, 0%, 15%); --table-border-color: hsl(0, 0%, 15%);
--table-head-border-color: hsl(0, 0%, 30%); --table-head-border-color: hsl(0, 0%, 30%);
} }
@@ -70,11 +81,29 @@
:root[data-theme="light"] { :root[data-theme="light"] {
--main-bg-color: hsl(210, 20%, 98%); --main-bg-color: hsl(210, 20%, 98%);
--main-text-color: hsl(0, 5%, 10%); --main-text-color: hsl(0, 5%, 10%);
--main-link-color: hsl(0, 100%, 35%); --main-link-color: hsl(220, 90%, 45%);
--accent-default: hsl(220, 90%, 45%);
--accent-alt: hsl(140, 45%, 28%);
--table-border-color: hsl(0, 0%, 85%); --table-border-color: hsl(0, 0%, 85%);
--table-head-border-color: hsl(0, 0%, 70%); --table-head-border-color: hsl(0, 0%, 70%);
} }
/* Accent-Alt-Override */
:root[data-accent="alt"]:not([data-theme="dark"]) {
--main-link-color: hsl(140, 45%, 28%);
--main-bg-color: hsl(35, 30%, 92%);
}
@media (prefers-color-scheme: dark) {
:root[data-accent="alt"]:not([data-theme]) {
--main-link-color: hsl(55, 100%, 60%);
--main-bg-color: hsl(260, 15%, 8%);
}
}
:root[data-theme="dark"][data-accent="alt"] {
--main-link-color: hsl(55, 100%, 60%);
--main-bg-color: hsl(260, 15%, 8%);
}
*, *::before, *::after { *, *::before, *::after {
box-sizing: border-box; box-sizing: border-box;
} }
@@ -106,6 +135,7 @@ body {
color: var(--main-text-color); color: var(--main-text-color);
background-color: var(--main-bg-color); background-color: var(--main-bg-color);
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
font-family: var(--main-font);
} }
header { header {
@@ -151,6 +181,7 @@ a:not([class]) {
a, a:visited { a, a:visited {
color: var(--main-link-color); color: var(--main-link-color);
text-decoration: none;
} }
a:focus { a:focus {
@@ -158,6 +189,10 @@ a:focus {
outline: 2px solid hsl(220, 90%, 52%); outline: 2px solid hsl(220, 90%, 52%);
} }
a:hover,
a:focus-visible {
text-decoration: underline;
}
h1 { h1 {
font-size: 2.5rem; font-size: 2.5rem;
@@ -218,7 +253,8 @@ ul.menuitems {
font-size: 0.9rem; font-size: 0.9rem;
} }
#theme-toggle { #theme-toggle,
#font-toggle {
background: none; background: none;
border: none; border: none;
padding: 0; padding: 0;
@@ -235,15 +271,42 @@ ul.menuitems {
} }
#theme-toggle:hover, #theme-toggle:hover,
#theme-toggle:focus-visible { #theme-toggle:focus-visible,
#font-toggle:hover,
#font-toggle:focus-visible {
opacity: 1; opacity: 1;
} }
#theme-toggle:focus-visible { #accent-toggle {
width: 0.9rem;
height: 0.9rem;
padding: 0;
border-radius: 50%;
background: linear-gradient(90deg, var(--accent-default) 50%, var(--accent-alt) 50%);
border: none;
cursor: pointer;
opacity: 0.85;
transition: opacity 0.15s ease, transform 0.15s ease;
}
#accent-toggle:hover,
#accent-toggle:focus-visible {
opacity: 1;
transform: scale(1.1);
}
#theme-toggle:focus-visible,
#accent-toggle:focus-visible,
#font-toggle:focus-visible {
outline: 2px solid hsl(220, 90%, 52%); outline: 2px solid hsl(220, 90%, 52%);
outline-offset: 2px; outline-offset: 2px;
} }
#font-toggle {
font-family: sans-serif;
font-size: 0.95rem;
}
#banner, #contentinfo { #banner, #contentinfo {
width: 100%; width: 100%;
text-align: center; text-align: center;
@@ -283,6 +346,38 @@ ul.menuitems {
content: "Categories:"; content: "Categories:";
} }
#tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
list-style: none;
max-width: 300px;
}
#tag-cloud:before {
content: "Tags:";
display: block;
width: 100%;
font-size: 1.4rem;
line-height: 1.8rem;
margin-bottom: 0.75rem;
}
#tag-cloud li {
margin: 0;
}
#tag-cloud a {
display: inline;
-webkit-line-clamp: unset;
overflow: visible;
}
#tag-cloud li.tag-weight-1 a { font-size: 0.7rem; opacity: 0.7; }
#tag-cloud li.tag-weight-2 a { font-size: 0.8rem; opacity: 0.85; }
#tag-cloud li.tag-weight-3 a { font-size: 0.9rem; opacity: 1; }
#tag-cloud li.tag-weight-4 a { font-size: 1rem; opacity: 1; font-weight: bold; }
#translations { #translations {
text-align: right; text-align: right;
} }
@@ -333,15 +428,9 @@ html[lang="de"] {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
line-height: 1.3; line-height: 1.3;
text-decoration: none;
padding-bottom: 1px; padding-bottom: 1px;
} }
.menuitems a:hover,
.menuitems a:focus-visible {
text-decoration: underline;
}
#content { #content {
flex: 1 1 auto; flex: 1 1 auto;
} }
@@ -413,14 +502,6 @@ footer.post-info {
color: var(--main-link-color); color: var(--main-link-color);
} }
.toc a {
text-decoration: none;
}
.toc a:hover {
text-decoration: underline;
}
/* /*
* Headings * Headings
*/ */
@@ -514,10 +595,23 @@ footer.post-info {
* Images * Images
*/ */
.entry-content img { img, svg {
max-width: 100%;
height: auto;
}
svg.injected-svg text,
svg.injected-svg tspan {
font-family: var(--main-font);
}
.entry-content img,
.entry-content svg.injected-svg {
counter-increment: figures; counter-increment: figures;
display: block; display: block;
max-width: 80%; max-width: 80%;
width: auto;
height: auto;
margin: 0 auto 0 auto; margin: 0 auto 0 auto;
padding: 1rem; padding: 1rem;
cursor: zoom-in; cursor: zoom-in;
@@ -565,15 +659,20 @@ footer.post-info {
max-height: 95vh; max-height: 95vh;
} }
.lightbox-overlay img { .lightbox-overlay #lightbox-media {
max-width: 95vw;
max-height: 85vh;
}
.lightbox-overlay #lightbox-media img,
.lightbox-overlay #lightbox-media svg {
max-width: 95vw; max-width: 95vw;
max-height: 85vh; max-height: 85vh;
object-fit: contain; object-fit: contain;
background: #fff;
} }
.lightbox-overlay #lightbox-caption { .lightbox-overlay #lightbox-caption {
color: #111; color: var(--main-text-color);
text-align: center; text-align: center;
margin-top: 0.75rem; margin-top: 0.75rem;
font-size: 0.95rem; font-size: 0.95rem;
+63
View File
@@ -0,0 +1,63 @@
document.addEventListener('DOMContentLoaded', function () {
const overlay = document.getElementById('lightbox-overlay');
const mediaContainer = document.getElementById('lightbox-media');
const overlayCaption = document.getElementById('lightbox-caption');
function openLightboxWithImg(src, alt, figureNumber, captionText) {
mediaContainer.innerHTML = '';
const img = document.createElement('img');
img.src = src;
img.alt = alt;
mediaContainer.appendChild(img);
setCaption(figureNumber, captionText);
overlay.classList.add('active');
}
function openLightboxWithSvg(svgEl, figureNumber, captionText) {
mediaContainer.innerHTML = '';
const svgClone = svgEl.cloneNode(true);
mediaContainer.appendChild(svgClone);
setCaption(figureNumber, captionText);
overlay.classList.add('active');
}
function setCaption(figureNumber, captionText) {
overlayCaption.innerHTML = captionText
? '<span class="fig-number">Figure ' + figureNumber + ':</span> ' + captionText
: '';
}
document.querySelectorAll('.entry-content img').forEach(function (img, index) {
const figureNumber = index + 1;
const originalSrc = img.src;
const originalAlt = img.alt;
const captionEl = img.nextElementSibling;
const captionText = (captionEl && captionEl.tagName === 'EM') ? captionEl.textContent : '';
img.addEventListener('click', function () {
openLightboxWithImg(originalSrc, originalAlt, figureNumber, captionText);
});
});
document.addEventListener('svg-injected', function (e) {
const svgEl = e.detail.svgEl;
svgEl.style.cursor = 'zoom-in';
svgEl.addEventListener('click', function () {
openLightboxWithSvg(svgEl, e.detail.figureNumber, e.detail.captionText);
});
});
overlay.addEventListener('click', function () {
overlay.classList.remove('active');
mediaContainer.innerHTML = '';
overlayCaption.innerHTML = '';
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
overlay.classList.remove('active');
mediaContainer.innerHTML = '';
overlayCaption.innerHTML = '';
}
});
});
+94
View File
@@ -0,0 +1,94 @@
document.addEventListener('DOMContentLoaded', function () {
const injectedSvgs = [];
function parseColorMap(cssText, prop) {
const map = {};
const regex = new RegExp('\\[' + prop + '="([^"]+)"\\]\\s*{\\s*' + prop + ':\\s*([^;]+);', 'g');
let m;
while ((m = regex.exec(cssText)) !== null) {
map[m[1]] = m[2].trim();
}
return map;
}
function applyThemeToSvg(entry, isDark) {
const { svgEl, fillMap, strokeMap } = entry;
Object.keys(fillMap).forEach(function (lightColor) {
svgEl.querySelectorAll('[fill="' + lightColor + '"]').forEach(function (el) {
el.setAttribute('fill', isDark ? fillMap[lightColor] : lightColor);
});
});
Object.keys(strokeMap).forEach(function (lightColor) {
svgEl.querySelectorAll('[stroke="' + lightColor + '"]').forEach(function (el) {
el.setAttribute('stroke', isDark ? strokeMap[lightColor] : lightColor);
});
});
}
function getIsDark() {
const attr = document.documentElement.getAttribute('data-theme');
if (attr) return attr === 'dark';
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
function applyThemeToAll() {
const isDark = getIsDark();
injectedSvgs.forEach(function (entry) {
applyThemeToSvg(entry, isDark);
});
}
const svgImages = document.querySelectorAll('.entry-content img[src$=".svg"]');
svgImages.forEach(function (img, index) {
const figureNumber = index + 1;
const originalSrc = img.src;
const originalAlt = img.alt;
const captionEl = img.nextElementSibling;
const captionText = (captionEl && captionEl.tagName === 'EM') ? captionEl.textContent : '';
fetch(img.src)
.then(function (response) { return response.text(); })
.then(function (svgText) {
const parser = new DOMParser();
const svgDoc = parser.parseFromString(svgText, 'image/svg+xml');
const svgEl = svgDoc.querySelector('svg');
if (!svgEl) return;
if (img.alt) {
svgEl.setAttribute('role', 'img');
svgEl.setAttribute('aria-label', img.alt);
}
svgEl.classList.add('injected-svg');
// Dark-Farbzuordnung aus dem internen <style>-Block auslesen, dann Block entfernen
const styleEl = svgEl.querySelector('style');
let fillMap = {};
let strokeMap = {};
if (styleEl) {
fillMap = parseColorMap(styleEl.textContent, 'fill');
strokeMap = parseColorMap(styleEl.textContent, 'stroke');
styleEl.remove();
}
img.replaceWith(svgEl);
const entry = { svgEl: svgEl, fillMap: fillMap, strokeMap: strokeMap };
injectedSvgs.push(entry);
applyThemeToSvg(entry, getIsDark());
document.dispatchEvent(new CustomEvent('svg-injected', {
detail: { svgEl: svgEl, originalSrc: originalSrc, originalAlt: originalAlt, figureNumber: figureNumber, captionText: captionText }
}));
})
.catch(function (err) {
console.error('SVG injection failed for', img.src, err);
});
});
document.addEventListener('themechange', applyThemeToAll);
});
+14
View File
@@ -0,0 +1,14 @@
(function () {
const storedTheme = localStorage.getItem('theme');
if (storedTheme === 'dark' || storedTheme === 'light') {
document.documentElement.setAttribute('data-theme', storedTheme);
}
const storedFont = localStorage.getItem('font');
if (storedFont === 'sans' || storedFont === 'serif') {
document.documentElement.setAttribute('data-font', storedFont);
}
const storedAccent = localStorage.getItem('accent');
if (storedAccent === 'alt' || storedAccent === 'default') {
document.documentElement.setAttribute('data-accent', storedAccent);
}
})();
+51
View File
@@ -0,0 +1,51 @@
document.addEventListener('DOMContentLoaded', function () {
const root = document.documentElement;
// Theme toggle
const toggleBtn = document.getElementById('theme-toggle');
function getCurrentTheme() {
const attr = root.getAttribute('data-theme');
if (attr) return attr;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function updateIcon() {
toggleBtn.textContent = getCurrentTheme() === 'dark' ? '☀' : '☾';
}
updateIcon();
toggleBtn.addEventListener('click', function () {
const next = getCurrentTheme() === 'dark' ? 'light' : 'dark';
root.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
updateIcon();
document.dispatchEvent(new CustomEvent('themechange'));
});
// Accent toggle
const accentToggleBtn = document.getElementById('accent-toggle');
function getCurrentAccent() {
return root.getAttribute('data-accent') === 'alt' ? 'alt' : 'default';
}
accentToggleBtn.addEventListener('click', function () {
const next = getCurrentAccent() === 'alt' ? 'default' : 'alt';
root.setAttribute('data-accent', next);
localStorage.setItem('accent', next);
document.dispatchEvent(new CustomEvent('themechange'));
});
// Font toggle
const fontToggleBtn = document.getElementById('font-toggle');
function getCurrentFont() {
return root.getAttribute('data-font') === 'sans' ? 'sans' : 'serif';
}
fontToggleBtn.addEventListener('click', function () {
const next = getCurrentFont() === 'sans' ? 'serif' : 'sans';
root.setAttribute('data-font', next);
localStorage.setItem('font', next);
});
});
+25 -69
View File
@@ -6,14 +6,9 @@
{% block head %} {% block head %}
<title>{% block title %}{{ SITENAME }}{% endblock title %}</title> <title>{% block title %}{{ SITENAME }}{% endblock title %}</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<script>
(function () { <script src="{{ main_siteurl | default(SITEURL) }}/theme/js/theme-init.js"></script>
const stored = localStorage.getItem('theme');
if (stored === 'dark' || stored === 'light') {
document.documentElement.setAttribute('data-theme', stored);
}
})();
</script>
<link rel="stylesheet" type="text/css" href="{{ main_siteurl | default(SITEURL) }}/theme/css/site.css" /> <link rel="stylesheet" type="text/css" href="{{ main_siteurl | default(SITEURL) }}/theme/css/site.css" />
{% if FEED_ALL_ATOM %} {% if FEED_ALL_ATOM %}
@@ -62,7 +57,11 @@
</ul> </ul>
<span class="header-delimiter">·</span> <span class="header-delimiter">·</span>
{% endif %} {% endif %}
<button id="font-toggle" aria-label="Toggle font">Aa</button>
<span class="header-delimiter">·</span>
<button id="theme-toggle" aria-label="Toggle color theme"></button> <button id="theme-toggle" aria-label="Toggle color theme"></button>
<span class="header-delimiter">·</span>
<button id="accent-toggle" aria-label="Toggle accent color"></button>
</div> </div>
<h1><a href="{{ SITEURL }}/">{{ SITENAME }} <strong>{{ SITESUBTITLE }}</strong></a></h1> <h1><a href="{{ SITEURL }}/">{{ SITENAME }} <strong>{{ SITESUBTITLE }}</strong></a></h1>
</header><!-- /#banner --> </header><!-- /#banner -->
@@ -97,6 +96,20 @@
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% if DISPLAY_TAG_CLOUD_ON_MENU | default(true) and tags is defined and tags %}
{% set tag_counts = tags | map('last') | map('length') | list %}
{% set max_count = tag_counts | max %}
{% if max_count and max_count > 0 %}
<ul id="tag-cloud" class="menuitems">
{% for tag, articles_in_tag in tags | sort %}
{% set weight = ((articles_in_tag | length) / max_count * 4) | round(0, 'ceil') | int %}
<li class="tag-weight-{{ weight }}">
<a href="{{ SITEURL }}/{{ tag.url }}">{{ tag }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% endif %}
</nav><!-- /#menu --> </nav><!-- /#menu -->
<main id="content"> <main id="content">
{% block content %} {% block content %}
@@ -112,71 +125,14 @@
<div class="lightbox-overlay" id="lightbox-overlay"> <div class="lightbox-overlay" id="lightbox-overlay">
<div class="lightbox-content"> <div class="lightbox-content">
<img id="lightbox-img" src="" alt=""> <div id="lightbox-media"></div>
<p id="lightbox-caption"></p> <p id="lightbox-caption"></p>
</div> </div>
</div> </div>
<script> <script src="{{ main_siteurl | default(SITEURL) }}/theme/js/lightbox.js" defer></script>
document.addEventListener('DOMContentLoaded', function () { <script src="{{ main_siteurl | default(SITEURL) }}/theme/js/toggles.js" defer></script>
const overlay = document.getElementById('lightbox-overlay'); <script src="{{ main_siteurl | default(SITEURL) }}/theme/js/svg-inject.js" defer></script>
const overlayImg = document.getElementById('lightbox-img');
const overlayCaption = document.getElementById('lightbox-caption');
document.querySelectorAll('.entry-content img').forEach(function (img, index) {
const figureNumber = index + 1;
img.addEventListener('click', function () {
overlayImg.src = img.src;
overlayImg.alt = img.alt;
const captionEl = img.nextElementSibling;
const captionText = (captionEl && captionEl.tagName === 'EM') ? captionEl.textContent : '';
overlayCaption.innerHTML = captionText
? '<span class="fig-number">Figure ' + figureNumber + ':</span> ' + captionText
: '';
overlay.classList.add('active');
});
});
overlay.addEventListener('click', function () {
overlay.classList.remove('active');
overlayImg.src = '';
overlayCaption.innerHTML = '';
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
overlay.classList.remove('active');
overlayImg.src = '';
overlayCaption.innerHTML = '';
}
});
});
</script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const toggleBtn = document.getElementById('theme-toggle');
const root = document.documentElement;
function getCurrentTheme() {
const attr = root.getAttribute('data-theme');
if (attr) return attr;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function updateIcon() {
toggleBtn.textContent = getCurrentTheme() === 'dark' ? '☀' : '☾';
}
updateIcon();
toggleBtn.addEventListener('click', function () {
const next = getCurrentTheme() === 'dark' ? 'light' : 'dark';
root.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
updateIcon();
});
});
</script>
</body> </body>
</html> </html>