Files
preland/components/theme-client.php
T

71 lines
4.5 KiB
PHP

<?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 read() {
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 (_) {}
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();
})(<?= 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>