Files
preland/components/theme-client.php
T

83 lines
5.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/** Общий inline runtime API/независимого PHP-преленда. Переменные задаёт вызывающий layout. */
$themeInitial = isset($themeInitial) && $themeInitial === 'dark' ? 'dark' : 'light';
$themeSurface = isset($themeSurface) && in_array($themeSurface, ['public', 'private', 'preland'], true) ? $themeSurface : 'public';
?>
<script>
(function (initial, surface) {
'use strict';
if (window.ZoomzillaTheme) { window.ZoomzillaTheme.refresh(); return; }
const key = 'theme_choice';
let shown = initial;
const canonical = value => typeof value === 'string' && value.length <= 128 && /^v1:(light|dark):(manual|preland):[A-Za-z0-9_-]{16,64}(?![\s\S])/.test(value) ? value : null;
const themeOf = value => value.split(':')[1];
function readCookie() {
try {
for (const part of document.cookie.split(';')) {
const cookie = part.trim();
if (!cookie.startsWith(key + '=')) continue;
const value = canonical(decodeURIComponent(cookie.slice(key.length + 1)));
if (value) return value;
}
} catch (_) {}
return null;
}
function read() {
const cookie = readCookie();
if (cookie) return cookie;
try { return canonical(window.localStorage.getItem(key)); } catch (_) { return null; }
}
function apply(theme) {
shown = theme;
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
if (document.body) document.body.classList.toggle('theme-dark', theme === 'dark');
document.querySelectorAll('[data-theme-choice]').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.themeChoice === theme)));
}
function uuid() {
if (window.crypto && typeof window.crypto.randomUUID === 'function') return window.crypto.randomUUID();
const bytes = new Uint8Array(16);
if (window.crypto && window.crypto.getRandomValues) window.crypto.getRandomValues(bytes);
else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
bytes[6] = (bytes[6] & 15) | 64; bytes[8] = (bytes[8] & 63) | 128;
const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
return hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' + hex.slice(16, 20) + '-' + hex.slice(20);
}
function refresh() { const value = read(); apply(value ? themeOf(value) : shown); }
function choose(theme) {
if (theme !== 'light' && theme !== 'dark') return;
const from = shown;
const value = 'v1:' + theme + ':manual:' + uuid().replace(/-/g, '');
try { document.cookie = key + '=' + encodeURIComponent(value) + '; Expires=Tue, 19 Jan 2038 03:14:07 GMT; Path=/; SameSite=Lax' + (location.protocol === 'https:' ? '; Secure' : ''); } catch (_) {}
try { window.localStorage.setItem(key, value); } catch (_) {}
apply(theme);
if (from === theme) return;
const detail = {schema_version: 1, event_id: uuid(), from, to: theme, source: 'manual', surface, host: location.hostname, path: location.pathname, occurred_at: new Date().toISOString()};
try { document.dispatchEvent(new CustomEvent('theme_changed', {detail})); } catch (_) {}
}
window.ZoomzillaTheme = Object.freeze({current: () => shown, choose, refresh});
// Pointer-клик не сбрасывает ввод/selection; клавиатурная навигация остаётся штатной.
document.addEventListener('mousedown', event => {
const button = event.target instanceof Element ? event.target.closest('[data-theme-choice]') : null;
const active = document.activeElement;
if (button && active && (active.matches('input,textarea') || active.isContentEditable)) event.preventDefault();
});
document.addEventListener('click', event => {
const button = event.target instanceof Element ? event.target.closest('[data-theme-choice]') : null;
if (button && !button.disabled) choose(button.dataset.themeChoice);
});
window.addEventListener('storage', event => { if (event.key === key || event.key === null) refresh(); });
window.addEventListener('pageshow', refresh);
document.addEventListener('DOMContentLoaded', refresh);
document.addEventListener('pjax:end', refresh);
refresh();
// Только первый boot зеркалит валидную cookie и уведомляет другие вкладки
// о server import. Read-only refresh/reinit не создают цикл storage events.
const bootCookie = readCookie();
if (bootCookie) {
apply(themeOf(bootCookie));
try { if (window.localStorage.getItem(key) !== bootCookie) window.localStorage.setItem(key, bootCookie); } catch (_) {}
}
})(<?= json_encode($themeInitial, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>, <?= json_encode($themeSurface, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>);
</script>