feat: theme switcher

This commit is contained in:
HECHT Axel
2025-01-15 12:19:50 +01:00
parent abbb4950a5
commit 62e9c32082
8 changed files with 352 additions and 22 deletions
+120
View File
@@ -2,6 +2,30 @@ import { setupPopovers } from './popover.js';
import { setupMasonries } from './masonry.js';
import { throttledDebounce, isElementVisible, openURLInNewTab } from './utils.js';
document.addEventListener('DOMContentLoaded', () => {
const theme = localStorage.getItem('theme');
if (!theme) {
return;
}
const html = document.querySelector('html');
const jsonTheme = JSON.parse(theme);
if (jsonTheme.themeScheme === 'light') {
html.classList.remove('dark-scheme');
html.classList.add('light-scheme');
} else if (jsonTheme.themeScheme === 'dark') {
html.classList.add('dark-scheme');
html.classList.remove('light-scheme');
}
html.classList.add(jsonTheme.theme);
document.querySelector('[name=color-scheme]').setAttribute('content', jsonTheme.themeScheme);
Array.from(document.querySelectorAll('.dropdown-button span')).forEach((button) => {
button.textContent = jsonTheme.theme;
})
})
async function fetchPageContent(pageData) {
// TODO: handle non 200 status codes/time outs
// TODO: add retries
@@ -638,6 +662,101 @@ function setupTruncatedElementTitles() {
}
}
/**
* @typedef {Object} HslColorField
* @property {number} Hue
* @property {number} Saturation
* @property {number} Lightness
*/
/**
* @typedef {Object} Theme
* @property {HslColorField} BackgroundColor
* @property {HslColorField} PrimaryColor
* @property {HslColorField} PositiveColor
* @property {HslColorField} NegativeColor
* @property {boolean} Light
* @property {number} ContrastMultiplier
* @property {number} TextSaturationMultiplier
*/
/**
* @typedef {Record<string, Theme>} ThemeCollection
*/
function setupThemeSwitcher() {
const presetsContainers = Array.from(document.querySelectorAll('.custom-presets'));
const userThemesKeys = Object.keys(userThemes);
presetsContainers.forEach((presetsContainer) => {
userThemesKeys.forEach(preset => {
const presetElement = document.createElement('div');
presetElement.className = 'theme-option';
presetElement.setAttribute('data-theme', preset);
presetElement.setAttribute('data-scheme', userThemes[preset].Light ? 'light' : 'dark');
presetElement.textContent = preset;
presetsContainer.appendChild(presetElement);
});
});
const dropdownButtons = Array.from(document.querySelectorAll('.dropdown-button'));
const dropdownContents = Array.from(document.querySelectorAll('.dropdown-content'));
dropdownButtons.forEach((dropdownButton) => {
dropdownButton.addEventListener('click', (e) => {
e.stopPropagation();
dropdownContents.forEach((dropdownContent) => {
dropdownContent.classList.toggle('show');
});
dropdownButton.classList.toggle('active');
});
});
document.addEventListener('click', (e) => {
if (!e.target.closest('.theme-dropdown')) {
dropdownContents.forEach((dropdownContent) => {
dropdownContent.classList.remove('show');
});
dropdownButtons.forEach((dropdownButton) => {
dropdownButton.classList.remove('active');
});
}
});
document.querySelectorAll('.theme-option').forEach(option => {
option.addEventListener('click', () => {
const selectedTheme = option.getAttribute('data-theme');
const selectedThemeScheme = option.getAttribute('data-scheme');
const previousTheme = localStorage.getItem('theme');
dropdownContents.forEach((dropdownContent) => {
dropdownContent.classList.remove('show');
});
dropdownButtons.forEach((dropdownButton) => {
const html = document.querySelector('html');
if (previousTheme) {
html.classList.remove(JSON.parse(previousTheme).theme);
}
dropdownButton.classList.remove('active');
dropdownButton.querySelector('span').textContent = option.textContent;
html.classList.add(selectedTheme);
if (selectedThemeScheme === 'light') {
html.classList.remove('dark-scheme');
html.classList.add('light-scheme');
} else if (selectedThemeScheme === 'dark') {
html.classList.add('dark-scheme');
html.classList.remove('light-scheme');
}
document.querySelector('[name=color-scheme]').setAttribute('content', selectedThemeScheme);
localStorage.setItem('theme', JSON.stringify({
theme: selectedTheme,
themeScheme: selectedThemeScheme
}));
});
});
});
}
async function setupPage() {
const pageElement = document.getElementById("page");
const pageContentElement = document.getElementById("page-content");
@@ -646,6 +765,7 @@ async function setupPage() {
pageContentElement.innerHTML = pageContent;
try {
setupThemeSwitcher();
setupPopovers();
setupClocks()
setupCarousels();
+102 -1
View File
@@ -55,6 +55,28 @@
--font-size-h6: 1.1rem;
}
.dark {
--scheme: ;
--bgh: 240;
--bgs: 8%;
--bgl: 9%;
--bghs: var(--bgh), var(--bgs);
--cm: 1;
--tsm: 1;
}
.light {
--scheme: 100% -;
--bgh: 240;
--bgs: 50%;
--bgl: 98%;
--bghs: var(--bgh), var(--bgs);
--cm: 1;
--tsm: 1;
--color-primary: hsl(43, 50%, 70%);
}
.light-scheme {
--scheme: 100% -;
}
@@ -1625,7 +1647,6 @@ details[open] .summary::after {
padding: 15px var(--content-bounds-padding);
display: flex;
align-items: center;
overflow-x: auto;
scrollbar-width: thin;
gap: 2.5rem;
}
@@ -1872,3 +1893,83 @@ details[open] .summary::after {
.list-gap-20 { --list-half-gap: 1rem; }
.list-gap-24 { --list-half-gap: 1.2rem; }
.list-gap-34 { --list-half-gap: 1.7rem; }
/*
### Theme Dropdown ###
*/
.theme-dropdown {
position: relative;
display: inline-block;
right: 0;
}
.dropdown-button {
padding: 10px 15px;
background: var(--color-widget-background);
border: 1px solid var(--color-widget-content-border);
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
min-width: 150px;
transition: border-color .2s;
color: var(--color-text-highlight);
}
.dropdown-button:hover {
border-color: var(--color-text-subdue);
}
.dropdown-content {
display: none;
position: absolute;
top: 100%;
left: 0;
background: var(--color-widget-content-border);
min-width: 150px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
border-radius: 4px;
z-index: 1000;
}
.mobile-navigation-page-links .dropdown-content {
top: unset;
bottom: 38px;
}
.dropdown-content.show {
display: block;
}
.theme-option {
padding: 10px 15px;
cursor: pointer;
transition: background-color 0.2s;
}
.theme-option:hover {
background-color: #f8f9fa;
}
.separator {
height: 1px;
background-color: #dee2e6;
margin: 5px 0;
}
.arrow {
border: solid #666;
border-width: 0 2px 2px 0;
display: inline-block;
padding: 3px;
transform: rotate(45deg);
transition: transform 0.2s ease;
margin-left: auto;
position: relative;
top: -1px;
}
.dropdown-button.active .arrow {
transform: rotate(-135deg);
}