feat(theme): локальный выбор и показанная тема handoff (#13)
This commit is contained in:
1 parent
3740839321
commit
9c06977e03
11 files changed
+163
-49
No files matched your search
@@ -0,0 +1,70 @@
|
||||
<?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) : initial); }
|
||||
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>
|
||||
Reference in new issue
Block a user