Files
site-portfolio/public/js/modules/theme/theme-controls.js
T
root 97ea48bc6f feat: per-theme accent + Activity WhatsApp(en)/GitHub cards, softer icon borders
- theme: store accent separately for light/dark (theme-core/controls)
- en contacts: WhatsApp card instead of VK (wa.me link)
- Activity: add GitHub card (logo webp) in both locales
- mp-card__image border uses --line to match card (less contrast)
2026-09-16 14:46:08 +02:00

199 lines
6.5 KiB
JavaScript

import { syncAriaRadioGroup } from "../shared/dom.js";
import { clampNum } from "../shared/math.js";
import { subscribePrefersColorSchemeChange } from "../shared/media.js";
import {
PICK,
S_RANGE,
L_RANGE,
RADIUS_IDS,
THEME_MODE_IDS,
effectiveThemeMode,
FONT_IDS,
MESH_BG_IDS,
applyThemeState,
drawSLPlane,
readThemeState,
updateKnob,
writeThemeState,
} from "./theme-core.js";
import { ensureThemeFontLoaded } from "./theme-fonts.js";
export function initThemeControls() {
const picker = document.querySelector("[data-theme-picker]");
const panel = document.querySelector("[data-theme-panel]");
const panelBtns = document.querySelectorAll("[data-theme-panel-btn]");
const themeModeBtns = document.querySelectorAll("[data-theme-mode]");
const canvas = document.querySelector("[data-sl-canvas]");
const knob = document.querySelector("[data-sl-knob]");
const hueSlider = document.querySelector("[data-hue-slider]");
const radiusBtns = document.querySelectorAll("[data-radius-option]");
const fontBtns = document.querySelectorAll("[data-font-option]");
const meshBgBtns = document.querySelectorAll("[data-mesh-bg-option]");
const themeOk =
picker && panel && panelBtns.length > 0 && themeModeBtns.length === 3 && canvas && hueSlider;
if (!themeOk) {
window.siteThemePanel = { open: () => {}, close: () => {}, toggle: () => {} };
return;
}
const state = readThemeState();
applyThemeState(state);
ensureThemeFontLoaded(state.font);
function acc() {
return state[effectiveThemeMode(state)] || state.light;
}
function syncHueSlider() {
hueSlider.value = String(Math.round(acc().h));
hueSlider.setAttribute("aria-valuenow", String(Math.round(acc().h)));
}
function persistAndApply() {
applyThemeState(state);
ensureThemeFontLoaded(state.font);
writeThemeState(state);
}
function bindOptionButtons(btns, attr, ids, stateKey) {
btns.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const id = btn.getAttribute(attr);
if (!ids.includes(id)) return;
state[stateKey] = id;
syncAriaRadioGroup(btns, attr, state[stateKey]);
persistAndApply();
});
});
}
syncHueSlider();
drawSLPlane(canvas, acc().h);
updateKnob(knob, acc().s, acc().l);
syncAriaRadioGroup(radiusBtns, "data-radius-option", state.radius);
syncAriaRadioGroup(themeModeBtns, "data-theme-mode", state.mode);
syncAriaRadioGroup(fontBtns, "data-font-option", state.font);
if (meshBgBtns.length >= 2) syncAriaRadioGroup(meshBgBtns, "data-mesh-bg-option", state.meshBg);
bindOptionButtons(radiusBtns, "data-radius-option", RADIUS_IDS, "radius");
bindOptionButtons(fontBtns, "data-font-option", FONT_IDS, "font");
bindOptionButtons(themeModeBtns, "data-theme-mode", THEME_MODE_IDS, "mode");
themeModeBtns.forEach((btn) =>
btn.addEventListener("click", () => {
if (!panel.hidden) {
syncHueSlider();
drawSLPlane(canvas, acc().h);
updateKnob(knob, acc().s, acc().l);
}
}),
);
if (meshBgBtns.length >= 2) bindOptionButtons(meshBgBtns, "data-mesh-bg-option", MESH_BG_IDS, "meshBg");
function setPanelOpen(open) {
panel.hidden = !open;
panelBtns.forEach((btn) => {
btn.setAttribute("aria-expanded", open ? "true" : "false");
btn.setAttribute("aria-label", open ? "Закрыть настройки стиля" : "Настройки стиля");
});
if (open) {
drawSLPlane(canvas, acc().h);
updateKnob(knob, acc().s, acc().l);
syncAriaRadioGroup(themeModeBtns, "data-theme-mode", state.mode);
syncAriaRadioGroup(fontBtns, "data-font-option", state.font);
if (meshBgBtns.length >= 2) syncAriaRadioGroup(meshBgBtns, "data-mesh-bg-option", state.meshBg);
}
}
subscribePrefersColorSchemeChange(() => {
if (state.mode !== "system") return;
applyThemeState(state);
if (!panel.hidden) {
syncHueSlider();
drawSLPlane(canvas, acc().h);
updateKnob(knob, acc().s, acc().l);
}
});
panelBtns.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
setPanelOpen(panel.hidden);
});
});
picker.addEventListener("click", (e) => e.stopPropagation());
document.addEventListener("click", (e) => {
const t = e.target;
if (!(t instanceof Node)) return;
const el = t instanceof Element ? t : t.parentElement;
if (el?.closest("[data-theme-panel-btn]")) return;
if (panel.contains(t) || picker.contains(t) || panel.hidden) return;
setPanelOpen(false);
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !panel.hidden) {
setPanelOpen(false);
e.preventDefault();
}
});
try {
const params = new URLSearchParams(window.location.search);
if (params.get("themePanel") === "open") requestAnimationFrame(() => setPanelOpen(true));
} catch {
/* ignore */
}
window.siteThemePanel = {
open: () => setPanelOpen(true),
close: () => setPanelOpen(false),
toggle: () => setPanelOpen(!!panel.hidden),
};
let canvasRect = null;
function pickFromClientXY(clientX, clientY) {
const r = canvasRect || canvas.getBoundingClientRect();
if (r.width <= 0 || r.height <= 0) return;
const x = clampNum(((clientX - r.left) / r.width) * 100, 0, 100, 0) / 100;
const y = clampNum(((clientY - r.top) / r.height) * 100, 0, 100, 0) / 100;
const a = acc();
a.s = Math.round(PICK.sMin + x * S_RANGE);
a.l = Math.round(PICK.lMin + (1 - y) * L_RANGE);
updateKnob(knob, a.s, a.l);
persistAndApply();
}
canvas.addEventListener("pointerdown", (e) => {
e.preventDefault();
canvasRect = canvas.getBoundingClientRect();
canvas.setPointerCapture(e.pointerId);
pickFromClientXY(e.clientX, e.clientY);
});
canvas.addEventListener("pointermove", (e) => {
if (!canvas.hasPointerCapture(e.pointerId)) return;
e.preventDefault();
pickFromClientXY(e.clientX, e.clientY);
});
canvas.addEventListener("pointerup", (e) => {
if (canvas.hasPointerCapture(e.pointerId)) canvas.releasePointerCapture(e.pointerId);
canvasRect = null;
});
hueSlider.addEventListener("input", () => {
const a = acc();
a.h = clampNum(hueSlider.value, 0, 360, a.h);
syncHueSlider();
drawSLPlane(canvas, a.h);
updateKnob(knob, a.s, a.l);
persistAndApply();
});
}