diff --git a/assets/css/dark-theme.css b/assets/css/dark-theme.css new file mode 100644 index 0000000..56832c5 --- /dev/null +++ b/assets/css/dark-theme.css @@ -0,0 +1,101 @@ +/** + * Dark palette for Twenty Twenty-Five default style variation. + * Overrides theme preset CSS variables when html.tt5-ldt-dark is set. + */ + +html.tt5-ldt-dark { + color-scheme: dark; + + --wp--preset--color--base: #111111; + --wp--preset--color--contrast: #f5f5f5; + --wp--preset--color--accent-1: #ffee58; + --wp--preset--color--accent-2: #d4a8d0; + --wp--preset--color--accent-3: #9b8fd4; + --wp--preset--color--accent-4: #a3a3a3; + --wp--preset--color--accent-5: #1c1c1a; + --wp--preset--color--accent-6: color-mix(in srgb, currentColor 25%, transparent); +} + +html.tt5-ldt-dark body, +html.tt5-ldt-dark .wp-site-blocks { + background-color: var(--wp--preset--color--base) !important; + color: var(--wp--preset--color--contrast) !important; +} + +/* Blocks that use preset background/text colors */ +html.tt5-ldt-dark .has-base-background-color { + background-color: var(--wp--preset--color--base) !important; +} + +html.tt5-ldt-dark .has-contrast-background-color { + background-color: var(--wp--preset--color--contrast) !important; +} + +html.tt5-ldt-dark .has-base-color { + color: var(--wp--preset--color--base) !important; +} + +html.tt5-ldt-dark .has-contrast-color { + color: var(--wp--preset--color--contrast) !important; +} + +html.tt5-ldt-dark .has-accent-1-color { + color: var(--wp--preset--color--accent-1) !important; +} + +html.tt5-ldt-dark .has-accent-2-color { + color: var(--wp--preset--color--accent-2) !important; +} + +html.tt5-ldt-dark .has-accent-3-color { + color: var(--wp--preset--color--accent-3) !important; +} + +html.tt5-ldt-dark .has-accent-4-color { + color: var(--wp--preset--color--accent-4) !important; +} + +html.tt5-ldt-dark .has-accent-5-color { + color: var(--wp--preset--color--accent-5) !important; +} + +html.tt5-ldt-dark .has-accent-1-background-color { + background-color: var(--wp--preset--color--accent-1) !important; +} + +html.tt5-ldt-dark .has-accent-2-background-color { + background-color: var(--wp--preset--color--accent-2) !important; +} + +html.tt5-ldt-dark .has-accent-3-background-color { + background-color: var(--wp--preset--color--accent-3) !important; +} + +html.tt5-ldt-dark .has-accent-4-background-color { + background-color: var(--wp--preset--color--accent-4) !important; +} + +html.tt5-ldt-dark .has-accent-5-background-color { + background-color: var(--wp--preset--color--accent-5) !important; +} + +/* Form controls follow dark scheme */ +html.tt5-ldt-dark input:not([type="submit"]):not([type="button"]):not([type="reset"]), +html.tt5-ldt-dark textarea, +html.tt5-ldt-dark select { + background-color: #1a1a1a; + color: var(--wp--preset--color--contrast); + border-color: var(--wp--preset--color--accent-4); +} + +html.tt5-ldt-dark ::selection { + background: var(--wp--preset--color--accent-3); + color: var(--wp--preset--color--contrast); +} + +/* Smooth transition when toggling */ +html.tt5-ldt-animate, +html.tt5-ldt-animate body, +html.tt5-ldt-animate .wp-site-blocks { + transition: background-color 0.25s ease, color 0.25s ease; +} diff --git a/assets/css/toggle.css b/assets/css/toggle.css new file mode 100644 index 0000000..b0d730b --- /dev/null +++ b/assets/css/toggle.css @@ -0,0 +1,63 @@ +.tt5-ldt-toggle { + position: fixed; + z-index: 99999; + right: 1.25rem; + bottom: 1.25rem; + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.65rem 1rem; + border: 1px solid color-mix(in srgb, currentColor 20%, transparent); + border-radius: 999px; + background: var(--wp--preset--color--base, #fff); + color: var(--wp--preset--color--contrast, #111); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + font: 600 0.875rem/1 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.tt5-ldt-toggle:hover { + transform: translateY(-2px); + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.2); +} + +.tt5-ldt-toggle:focus-visible { + outline: 2px solid var(--wp--preset--color--accent-3, #503aa8); + outline-offset: 3px; +} + +.tt5-ldt-toggle__icon { + font-size: 1.1rem; + line-height: 1; +} + +html:not(.tt5-ldt-dark) .tt5-ldt-toggle__icon--moon, +html.tt5-ldt-dark .tt5-ldt-toggle__icon--sun { + display: none; +} + +html.tt5-ldt-dark .tt5-ldt-toggle { + background: #1a1a1a; + color: #f5f5f5; + border-color: rgba(255, 255, 255, 0.15); +} + +@media (max-width: 480px) { + .tt5-ldt-toggle__label { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } + + .tt5-ldt-toggle { + padding: 0.75rem; + border-radius: 50%; + } +} diff --git a/assets/js/toggle.js b/assets/js/toggle.js new file mode 100644 index 0000000..d440a74 --- /dev/null +++ b/assets/js/toggle.js @@ -0,0 +1,125 @@ +(function () { + 'use strict'; + + var config = window.tt5Ldt || {}; + var btn = document.getElementById('tt5-ldt-toggle'); + if (!btn) return; + + var storageKey = config.storageKey || 'tt5_ldt_scheme'; + var cookieName = config.cookieName || 'tt5_ldt_scheme'; + var prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); + + function getStoredScheme() { + try { + var stored = localStorage.getItem(storageKey); + if (stored === 'light' || stored === 'dark') { + return stored; + } + } catch (e) {} + + var match = document.cookie.match( + new RegExp('(?:^|; )' + cookieName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=([^;]*)') + ); + if (match) { + var cookieVal = decodeURIComponent(match[1]); + if (cookieVal === 'light' || cookieVal === 'dark') { + return cookieVal; + } + } + + return null; + } + + function getScheme() { + var stored = getStoredScheme(); + if (stored) { + return stored; + } + return document.documentElement.classList.contains('tt5-ldt-dark') + ? 'dark' + : 'light'; + } + + function setCookie(scheme) { + var path = config.cookiePath || '/'; + var domain = config.cookieDomain ? '; domain=' + config.cookieDomain : ''; + var maxAge = 60 * 60 * 24 * 365; + document.cookie = + cookieName + + '=' + + encodeURIComponent(scheme) + + ';path=' + + path + + domain + + ';max-age=' + + maxAge + + ';SameSite=Lax'; + } + + function applyScheme(scheme, animate) { + var root = document.documentElement; + var isDark = scheme === 'dark'; + + if (animate) { + root.classList.add('tt5-ldt-animate'); + window.setTimeout(function () { + root.classList.remove('tt5-ldt-animate'); + }, 300); + } + + root.classList.toggle('tt5-ldt-dark', isDark); + root.setAttribute('data-tt5-scheme', scheme); + + try { + localStorage.setItem(storageKey, scheme); + } catch (e) {} + + setCookie(scheme); + updateButton(isDark); + + if (config.isLoggedIn && config.ajaxUrl && config.nonce) { + var body = new FormData(); + body.append('action', 'tt5_ldt_save_scheme'); + body.append('nonce', config.nonce); + body.append('scheme', scheme); + fetch(config.ajaxUrl, { method: 'POST', body: body, credentials: 'same-origin' }); + } + } + + function updateButton(isDark) { + var i18n = config.i18n || {}; + btn.setAttribute('aria-pressed', isDark ? 'true' : 'false'); + btn.setAttribute( + 'aria-label', + isDark ? i18n.light || 'Switch to light mode' : i18n.dark || 'Switch to dark mode' + ); + btn.setAttribute( + 'title', + isDark ? i18n.lightLabel || 'Light mode' : i18n.darkLabel || 'Dark mode' + ); + + var label = btn.querySelector('.tt5-ldt-toggle__label'); + if (label) { + label.textContent = isDark ? i18n.lightLabel || 'Light' : i18n.darkLabel || 'Dark'; + } + } + + btn.addEventListener('click', function () { + var next = getScheme() === 'dark' ? 'light' : 'dark'; + applyScheme(next, true); + }); + + // First visit: no saved preference — follow system setting until user toggles. + if (!getStoredScheme() && !document.documentElement.getAttribute('data-tt5-scheme')) { + applyScheme(prefersDark.matches ? 'dark' : 'light', false); + } else { + updateButton(document.documentElement.classList.contains('tt5-ldt-dark')); + } + + prefersDark.addEventListener('change', function (event) { + if (getStoredScheme()) { + return; + } + applyScheme(event.matches ? 'dark' : 'light', true); + }); +})(); diff --git a/includes/class-tt5-ldt.php b/includes/class-tt5-ldt.php new file mode 100644 index 0000000..912dc81 --- /dev/null +++ b/includes/class-tt5-ldt.php @@ -0,0 +1,253 @@ +get_template(); + } + + /** + * Warn in admin if the theme is not supported. + */ + public static function admin_theme_notice() { + if ( self::is_target_theme() || ! current_user_can( 'switch_themes' ) ) { + return; + } + + $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; + if ( $screen && 'plugins' !== $screen->id ) { + return; + } + + echo '
'; + echo esc_html__( + 'TT5 Light/Dark Toggle is active but Twenty Twenty-Five is not the current theme. The toggle only appears with that theme.', + 'tt5-light-dark-toggle' + ); + echo '
| + | + + + | +
|---|