From 833a6dba1f3a968e90e58b9f76468fef15dc03e0 Mon Sep 17 00:00:00 2001 From: S1M0N38 Date: Wed, 30 Apr 2025 13:20:17 +0200 Subject: [PATCH] feat: dark/light auto sytem theme There is a couple of issue requesting light/dark mode: - https://github.com/panr/hugo-theme-terminal/issues/532 - https://github.com/panr/hugo-theme-terminal/issues/471 A simple way to let the browser select between dark and light theme based on system theme is to use `prefers-color-scheme` css attribute. --- index.html | 45 +++++++++++++++---- modules/components.js | 54 +++++++++++++++++++--- modules/presets.js | 102 ++++++++++++++++++++++++++++++------------ script.js | 62 +++++++++++++++++++++---- 4 files changed, 212 insertions(+), 51 deletions(-) diff --git a/index.html b/index.html index f40787d..8406a6b 100644 --- a/index.html +++ b/index.html @@ -72,22 +72,51 @@

Terminal.css

-
Color Schema Presets
-
+
Color Schema Presets (light)
+
+
+
+
+
+
Color Schema Presets (dark)
+
+
+
+
+
+
Background (light)
+
+
+
+
Foreground (light)
+
+
+
+
Accent (light)
+
-
Background
-
+
Background (dark)
+
-
Foreground
-
+
Foreground (dark)
+
-
Accent
-
+
Accent (dark)
+
+
+
+
+
+
Preview Theme
+
+ + +
diff --git a/modules/components.js b/modules/components.js index 7544d48..bc030dc 100644 --- a/modules/components.js +++ b/modules/components.js @@ -13,13 +13,35 @@ export class Standalone { } updateVariables() { + // Get light theme values (use names without '-light' suffix for standalone) + const bgLight = this.formData.get("background-light") || defaultValues["background"]; + const fgLight = this.formData.get("foreground-light") || defaultValues["foreground"]; + const accentLight = this.formData.get("accent-light") || defaultValues["accent"]; + + // Get dark theme values + const bgDark = this.formData.get("background-dark") || bgLight; // Default dark to light if not provided + const fgDark = this.formData.get("foreground-dark") || fgLight; + const accentDark = this.formData.get("accent-dark") || accentLight; + + // Construct the hybrid CSS variables block const variables = `:root { - --background: ${this.formData.get("background")}; - --foreground: ${this.formData.get("foreground")}; - --accent: ${this.formData.get("accent")}; + --background: ${bgLight}; + --foreground: ${fgLight}; + --accent: ${accentLight}; --radius: ${defaultValues["radius"]}; --font-size: ${defaultValues["fontSize"]}; --line-height: ${defaultValues["lineHeight"]}; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: ${bgDark}; + --foreground: ${fgDark}; + --accent: ${accentDark}; + --radius: ${defaultValues["radius"]}; + --font-size: ${defaultValues["fontSize"]}; + --line-height: ${defaultValues["lineHeight"]}; + } }`; this.styles = this.styles.replace(components.variables, variables.trim()); return this; @@ -93,11 +115,31 @@ export class TerminalTheme { } updateVariables() { + // Get light theme values + const bgLight = this.formData.get("background-light") || defaultValues["background"]; + const fgLight = this.formData.get("foreground-light") || defaultValues["foreground"]; + const accentLight = this.formData.get("accent-light") || defaultValues["accent"]; + + // Get dark theme values + const bgDark = this.formData.get("background-dark") || bgLight; // Default dark to light if not provided + const fgDark = this.formData.get("foreground-dark") || fgLight; + const accentDark = this.formData.get("accent-dark") || accentLight; + + // Construct the hybrid CSS variables block const variables = `:root { - --background: ${this.formData.get("background")}; - --foreground: ${this.formData.get("foreground")}; - --accent: ${this.formData.get("accent")}; + --background: ${bgLight}; + --foreground: ${fgLight}; + --accent: ${accentLight}; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: ${bgDark}; + --foreground: ${fgDark}; + --accent: ${accentDark}; + } }`; + // Replace the placeholder in the base styles this.styles = this.styles.replace(components.variables, variables.trim()); return this; } diff --git a/modules/presets.js b/modules/presets.js index 3e7d872..175b106 100644 --- a/modules/presets.js +++ b/modules/presets.js @@ -1,45 +1,89 @@ import { isDark } from "./helpers.js"; -export const presetsInput = document.querySelector("#presets"); +// Get references to both preset dropdowns +const presetsInputLight = document.querySelector("#presets-light"); +const presetsInputDark = document.querySelector("#presets-dark"); +const colorInputsLight = { + background: document.querySelector('input[name="background-light"]'), + foreground: document.querySelector('input[name="foreground-light"]'), + accent: document.querySelector('input[name="accent-light"]'), +}; +const colorInputsDark = { + background: document.querySelector('input[name="background-dark"]'), + foreground: document.querySelector('input[name="foreground-dark"]'), + accent: document.querySelector('input[name="accent-dark"]'), +}; + const res = await fetch("./presets.json"); const presets = await res.json(); -// At the moment it takes less than 1ms to create a grouped list of presets. -// If it gets worse over time, I'll rewrite it. -const presetsList = []; -for (const [k, v] of Object.entries(presets)) { - const entry = v; - entry.name = k; - presetsList.push(entry); -} +// Populate function +function populatePresets(selectElement) { + // Add a default "Select Preset..." option + const defaultOption = new Option("Select Preset...", ""); + defaultOption.disabled = true; + defaultOption.selected = true; + selectElement.add(defaultOption); -const grouped = Object.groupBy(presetsList, ({ background }) => { - return isDark(background) ? "Dark" : "Light"; -}); + const presetsList = []; + for (const [k, v] of Object.entries(presets)) { + const entry = { ...v, name: k }; // Clone and add name + presetsList.push(entry); + } -for (const [group, p] of Object.entries(grouped)) { - const groupOption = new Option(group, group); - groupOption.disabled = true; - presetsInput.add(groupOption, undefined); + const grouped = Object.groupBy(presetsList, ({ background }) => { + return isDark(background) ? "Dark" : "Light"; + }); - for (const v of p) { - const option = new Option(v.name, v.name); - presetsInput.add(option, undefined); + // Ensure consistent group order (Light first) + const groupOrder = ["Light", "Dark"]; + for (const group of groupOrder) { + if (grouped[group]) { + const groupOption = new Option(group, group); + groupOption.disabled = true; + selectElement.add(groupOption, undefined); + + for (const v of grouped[group]) { + const option = new Option(v.name, v.name); + selectElement.add(option, undefined); + } + } } } -// ----------------------------------------------------------------------------- -const root = document.querySelector(":root"); -const settings = document.querySelectorAll("#settings input"); +// Populate both dropdowns +populatePresets(presetsInputLight); +populatePresets(presetsInputDark); -presetsInput.addEventListener("change", e => { - const preset = presets[e.currentTarget.value]; +// Event listener for light presets +presetsInputLight.addEventListener("change", e => { + const presetName = e.currentTarget.value; + if (!presetName || !presets[presetName]) return; // Ignore if default or invalid - for (const i of settings) { - if (preset[i.name]) { - i.value = preset[i.name]; - root.style.setProperty(`--${i.name}`, preset[i.name]); - } + const preset = presets[presetName]; + if (preset.background) colorInputsLight.background.value = preset.background; + if (preset.foreground) colorInputsLight.foreground.value = preset.foreground; + if (preset.accent) colorInputsLight.accent.value = preset.accent; + + // Trigger live preview update (function defined in script.js) + if (window.updateLivePreview) { + window.updateLivePreview(); + } +}); + +// Event listener for dark presets +presetsInputDark.addEventListener("change", e => { + const presetName = e.currentTarget.value; + if (!presetName || !presets[presetName]) return; // Ignore if default or invalid + + const preset = presets[presetName]; + if (preset.background) colorInputsDark.background.value = preset.background; + if (preset.foreground) colorInputsDark.foreground.value = preset.foreground; + if (preset.accent) colorInputsDark.accent.value = preset.accent; + + // Trigger live preview update (function defined in script.js) + if (window.updateLivePreview) { + window.updateLivePreview(); } }); diff --git a/script.js b/script.js index 6a8eff4..d40c2b0 100644 --- a/script.js +++ b/script.js @@ -7,18 +7,64 @@ import "./modules/presets.js"; // Init ------------------------------------------------------------------------ const root = document.querySelector(":root"); -const settings = document.querySelectorAll("#settings input"); -for (const i of settings) { - if (defaultValues[i.name]) { - i.value = defaultValues[i.name]; - } +// Get references to all relevant inputs once +const colorInputsLight = { + background: document.querySelector('input[name="background-light"]'), + foreground: document.querySelector('input[name="foreground-light"]'), + accent: document.querySelector('input[name="accent-light"]'), +}; +const colorInputsDark = { + background: document.querySelector('input[name="background-dark"]'), + foreground: document.querySelector('input[name="foreground-dark"]'), + accent: document.querySelector('input[name="accent-dark"]'), +}; +const previewThemeRadios = document.querySelectorAll('input[name="preview-theme"]'); +const headingStyleSelect = document.querySelector('select[name="headingStyle"]'); +const fontFamilySelect = document.querySelector('select[name="fontFamily"]'); + +// Set initial default values for color inputs +if (defaultValues["background"]) colorInputsLight.background.value = defaultValues["background"]; +if (defaultValues["foreground"]) colorInputsLight.foreground.value = defaultValues["foreground"]; +if (defaultValues["accent"]) colorInputsLight.accent.value = defaultValues["accent"]; +// Optionally set defaults for dark theme as well, or leave them empty/black/white +// For now, let's mirror the light theme defaults initially +if (defaultValues["background"]) colorInputsDark.background.value = defaultValues["background"]; +if (defaultValues["foreground"]) colorInputsDark.foreground.value = defaultValues["foreground"]; +if (defaultValues["accent"]) colorInputsDark.accent.value = defaultValues["accent"]; + +// Set other defaults if they exist in defaultValues +if (headingStyleSelect && defaultValues[headingStyleSelect.name]) { + headingStyleSelect.value = defaultValues[headingStyleSelect.name]; } +if (fontFamilySelect && defaultValues[fontFamilySelect.name]) { + fontFamilySelect.value = defaultValues[fontFamilySelect.name]; +} + // ----------------------------------------------------------------------------- -function setVariable(variable, value) { - root.style.setProperty(variable, value); +// Live Preview Update Function +function updateLivePreview() { + const selectedTheme = document.querySelector('input[name="preview-theme"]:checked').value; + let sourceInputs; + + if (selectedTheme === 'dark') { + sourceInputs = colorInputsDark; + } else { + sourceInputs = colorInputsLight; + } + + root.style.setProperty('--background', sourceInputs.background.value); + root.style.setProperty('--foreground', sourceInputs.foreground.value); + root.style.setProperty('--accent', sourceInputs.accent.value); + + // Note: Other variables like --radius, --font-size, --line-height are not + // dynamically updated in this preview but will be included in the download. + // If previewing them is needed, this function would need expansion. } -window.setVariable = setVariable; +window.updateLivePreview = updateLivePreview; // Make it globally accessible + +// Initial preview update on load +updateLivePreview(); // Submit Download const form = document.querySelector("#settings");