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)
This commit is contained in:
1 parent
302855efcf
commit
97ea48bc6f
25 files changed
+235
-47
No files matched your search
@@ -7,6 +7,7 @@ import {
|
||||
L_RANGE,
|
||||
RADIUS_IDS,
|
||||
THEME_MODE_IDS,
|
||||
effectiveThemeMode,
|
||||
FONT_IDS,
|
||||
MESH_BG_IDS,
|
||||
applyThemeState,
|
||||
@@ -40,9 +41,13 @@ export function initThemeControls() {
|
||||
applyThemeState(state);
|
||||
ensureThemeFontLoaded(state.font);
|
||||
|
||||
function acc() {
|
||||
return state[effectiveThemeMode(state)] || state.light;
|
||||
}
|
||||
|
||||
function syncHueSlider() {
|
||||
hueSlider.value = String(Math.round(state.h));
|
||||
hueSlider.setAttribute("aria-valuenow", String(Math.round(state.h)));
|
||||
hueSlider.value = String(Math.round(acc().h));
|
||||
hueSlider.setAttribute("aria-valuenow", String(Math.round(acc().h)));
|
||||
}
|
||||
|
||||
function persistAndApply() {
|
||||
@@ -65,8 +70,8 @@ export function initThemeControls() {
|
||||
}
|
||||
|
||||
syncHueSlider();
|
||||
drawSLPlane(canvas, state.h);
|
||||
updateKnob(knob, state.s, state.l);
|
||||
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);
|
||||
@@ -75,6 +80,15 @@ export function initThemeControls() {
|
||||
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) {
|
||||
@@ -84,8 +98,8 @@ export function initThemeControls() {
|
||||
btn.setAttribute("aria-label", open ? "Закрыть настройки стиля" : "Настройки стиля");
|
||||
});
|
||||
if (open) {
|
||||
drawSLPlane(canvas, state.h);
|
||||
updateKnob(knob, state.s, state.l);
|
||||
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);
|
||||
@@ -95,6 +109,11 @@ export function initThemeControls() {
|
||||
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) => {
|
||||
@@ -143,9 +162,10 @@ export function initThemeControls() {
|
||||
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;
|
||||
state.s = Math.round(PICK.sMin + x * S_RANGE);
|
||||
state.l = Math.round(PICK.lMin + (1 - y) * L_RANGE);
|
||||
updateKnob(knob, state.s, state.l);
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -168,10 +188,11 @@ export function initThemeControls() {
|
||||
});
|
||||
|
||||
hueSlider.addEventListener("input", () => {
|
||||
state.h = clampNum(hueSlider.value, 0, 360, state.h);
|
||||
const a = acc();
|
||||
a.h = clampNum(hueSlider.value, 0, 360, a.h);
|
||||
syncHueSlider();
|
||||
drawSLPlane(canvas, state.h);
|
||||
updateKnob(knob, state.s, state.l);
|
||||
drawSLPlane(canvas, a.h);
|
||||
updateKnob(knob, a.s, a.l);
|
||||
persistAndApply();
|
||||
});
|
||||
}
|
||||
@@ -27,7 +27,14 @@ export const S_RANGE = PICK.sMax - PICK.sMin;
|
||||
export const L_RANGE = PICK.lMax - PICK.lMin;
|
||||
|
||||
export function defaultThemeState() {
|
||||
return { mode: "system", h: 210, s: 90, l: 40, radius: "medium", font: "geologica", meshBg: "on" };
|
||||
return {
|
||||
mode: "system",
|
||||
light: { h: 210, s: 90, l: 40 },
|
||||
dark: { h: 210, s: 90, l: 40 },
|
||||
radius: "medium",
|
||||
font: "geologica",
|
||||
meshBg: "on",
|
||||
};
|
||||
}
|
||||
|
||||
export function effectiveThemeMode(state) {
|
||||
@@ -35,23 +42,53 @@ export function effectiveThemeMode(state) {
|
||||
return state.mode;
|
||||
}
|
||||
|
||||
export function activeAccent(state) {
|
||||
const key = effectiveThemeMode(state);
|
||||
return state[key] || state.light || { h: 210, s: 90, l: 40 };
|
||||
}
|
||||
|
||||
function parseAccent(obj, base) {
|
||||
if (!obj) return { ...base };
|
||||
return {
|
||||
h: clampNum(obj.h ?? obj.hue, 0, 360, base.h),
|
||||
s: clampNum(obj.s ?? obj.sat, PICK.sMin, PICK.sMax, base.s),
|
||||
l: clampNum(obj.l ?? obj.light, PICK.lMin, PICK.lMax, base.l),
|
||||
};
|
||||
}
|
||||
|
||||
export function readThemeState() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return defaultThemeState();
|
||||
const o = JSON.parse(raw);
|
||||
const base = defaultThemeState();
|
||||
const hRaw = o.h ?? o.hue;
|
||||
const h = clampNum(hRaw, 0, 360, base.h);
|
||||
const s = clampNum(o.s ?? o.sat, PICK.sMin, PICK.sMax, base.s);
|
||||
const lRaw = o.l ?? o.light;
|
||||
const l = clampNum(lRaw, PICK.lMin, PICK.lMax, base.l);
|
||||
const mode =
|
||||
o.mode === "light" || o.mode === "dark" || o.mode === "system" ? o.mode : base.mode;
|
||||
const radius = RADIUS_IDS.includes(o.radius) ? o.radius : base.radius;
|
||||
const font = normalizeFontId(o.font ?? base.font);
|
||||
const meshBg = MESH_BG_IDS.includes(o.meshBg) ? o.meshBg : base.meshBg;
|
||||
return { mode, h, s, l, radius, font, meshBg };
|
||||
|
||||
let light = base.light;
|
||||
let dark = base.dark;
|
||||
if (o.light || o.dark) {
|
||||
light = parseAccent(o.light, base.light);
|
||||
dark = parseAccent(o.dark, base.dark);
|
||||
} else if (
|
||||
o.h !== undefined ||
|
||||
o.hue !== undefined ||
|
||||
o.s !== undefined ||
|
||||
o.sat !== undefined ||
|
||||
o.l !== undefined ||
|
||||
o.light !== undefined
|
||||
) {
|
||||
// legacy: единый акцент → копируем в обе темы
|
||||
const h = clampNum(o.h ?? o.hue, 0, 360, base.light.h);
|
||||
const s = clampNum(o.s ?? o.sat, PICK.sMin, PICK.sMax, base.light.s);
|
||||
const l = clampNum(o.l ?? o.light, PICK.lMin, PICK.lMax, base.light.l);
|
||||
light = { h, s, l };
|
||||
dark = { h, s, l };
|
||||
}
|
||||
return { mode, light, dark, radius, font, meshBg };
|
||||
} catch {
|
||||
return defaultThemeState();
|
||||
}
|
||||
@@ -62,9 +99,8 @@ export function writeThemeState(state) {
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
mode: state.mode,
|
||||
h: state.h,
|
||||
s: state.s,
|
||||
l: state.l,
|
||||
light: state.light,
|
||||
dark: state.dark,
|
||||
radius: state.radius,
|
||||
font: state.font,
|
||||
meshBg: state.meshBg,
|
||||
@@ -110,14 +146,16 @@ export function relativeLuminance(rgb) {
|
||||
|
||||
export function applyThemeState(state) {
|
||||
const root = document.documentElement;
|
||||
root.dataset.theme = effectiveThemeMode(state);
|
||||
const mode = effectiveThemeMode(state);
|
||||
root.dataset.theme = mode;
|
||||
root.dataset.radius = state.radius;
|
||||
root.dataset.font = state.font;
|
||||
root.dataset.meshBg = state.meshBg === "off" ? "off" : "on";
|
||||
root.style.setProperty("--theme-h", String(state.h));
|
||||
root.style.setProperty("--theme-s", String(state.s));
|
||||
root.style.setProperty("--theme-l", String(state.l));
|
||||
const rgb = hslToRgb(state.h, state.s, state.l);
|
||||
const acc = activeAccent(state);
|
||||
root.style.setProperty("--theme-h", String(acc.h));
|
||||
root.style.setProperty("--theme-s", String(acc.s));
|
||||
root.style.setProperty("--theme-l", String(acc.l));
|
||||
const rgb = hslToRgb(acc.h, acc.s, acc.l);
|
||||
const lum = relativeLuminance(rgb);
|
||||
root.style.setProperty("--on-accent", lum > 0.58 ? "hsl(220 14% 12%)" : "hsl(0 0% 98%)");
|
||||
window.dispatchEvent(new CustomEvent("site-theme-applied", { bubbles: true }));
|
||||
|
||||
Reference in new issue
Block a user