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:
root committed 2026-09-16 14:46:08 +02:00
1 parent 302855efcf
commit 97ea48bc6f
25 files changed
+235 -47

No files matched your search

+53 -15
View File
@@ -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 }));