From 82fddde3150ceb3cacebee4a47ce0545a2f84d68 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sun, 4 Jan 2026 17:08:50 +0330 Subject: [PATCH] fix menu color --- src/components/ThemeColorSetter.tsx | 35 +++------ src/components/wrapper/PreferenceWrapper.tsx | 35 +++------ src/hooks/helpers/useThemeColor.ts | 54 ++----------- src/lib/helpers/colorUtils.ts | 80 ++++++++++++++++++++ 4 files changed, 103 insertions(+), 101 deletions(-) create mode 100644 src/lib/helpers/colorUtils.ts diff --git a/src/components/ThemeColorSetter.tsx b/src/components/ThemeColorSetter.tsx index 9b5c025..5f31e3f 100644 --- a/src/components/ThemeColorSetter.tsx +++ b/src/components/ThemeColorSetter.tsx @@ -1,39 +1,22 @@ 'use client' import { useEffect } from 'react' +import { hexToOklch, calculateBrightness } from '@/lib/helpers/colorUtils' export function ThemeColorSetter() { useEffect(() => { try { const savedColor = localStorage.getItem('theme-primary-color') if (savedColor) { - const hex = savedColor.replace('#', '') - const r = parseInt(hex.substring(0, 2), 16) / 255 - const g = parseInt(hex.substring(2, 4), 16) / 255 - const b = parseInt(hex.substring(4, 6), 16) / 255 - - const lightness = 0.2126 * r + 0.7152 * g + 0.0722 * b - const max = Math.max(r, g, b) - const min = Math.min(r, g, b) - const chroma = (max - min) * 0.15 - - let hue = 0 - if (chroma !== 0) { - if (max === r) { - hue = ((g - b) / (max - min) + (g < b ? 6 : 0)) * 60 - } else if (max === g) { - hue = ((b - r) / (max - min) + 2) * 60 - } else { - hue = ((r - g) / (max - min) + 4) * 60 - } + const oklchColor = hexToOklch(savedColor) + + if (oklchColor) { + document.documentElement.style.setProperty('--primary', oklchColor) + + const brightness = calculateBrightness(savedColor) + const foregroundOklch = brightness > 0.5 ? 'oklch(0 0 0)' : 'oklch(1 0 0)' + document.documentElement.style.setProperty('--primary-foreground', foregroundOklch) } - - const oklchColor = `oklch(${lightness.toFixed(3)} ${chroma.toFixed(3)} ${hue.toFixed(1)})` - document.documentElement.style.setProperty('--primary', oklchColor) - - const brightness = (r * 299 + g * 587 + b * 114) / 1000 - const foregroundOklch = brightness > 0.5 ? 'oklch(0 0 0)' : 'oklch(1 0 0)' - document.documentElement.style.setProperty('--primary-foreground', foregroundOklch) } } catch (e) { // Silent fail diff --git a/src/components/wrapper/PreferenceWrapper.tsx b/src/components/wrapper/PreferenceWrapper.tsx index f87a27e..1cf3393 100644 --- a/src/components/wrapper/PreferenceWrapper.tsx +++ b/src/components/wrapper/PreferenceWrapper.tsx @@ -4,6 +4,7 @@ import usePreference from '@/hooks/helpers/usePreference'; import React, { useEffect, useState } from 'react'; import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData'; import SplashScreen from '@/components/overlays/SplashScreen'; +import { hexToOklch, calculateBrightness } from '@/lib/helpers/colorUtils'; type Props = { children: React.ReactNode @@ -12,33 +13,15 @@ type Props = { // تابع برای اعمال رنگ primary function applyPrimaryColor(colorHex: string) { const root = document.documentElement; - const hex = colorHex.replace('#', ''); - const r = parseInt(hex.substring(0, 2), 16) / 255; - const g = parseInt(hex.substring(2, 4), 16) / 255; - const b = parseInt(hex.substring(4, 6), 16) / 255; - - const lightness = 0.2126 * r + 0.7152 * g + 0.0722 * b; - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const chroma = (max - min) * 0.15; - - let hue = 0; - if (chroma !== 0) { - if (max === r) { - hue = ((g - b) / (max - min) + (g < b ? 6 : 0)) * 60; - } else if (max === g) { - hue = ((b - r) / (max - min) + 2) * 60; - } else { - hue = ((r - g) / (max - min) + 4) * 60; - } + const oklchColor = hexToOklch(colorHex); + + if (oklchColor) { + root.style.setProperty('--primary', oklchColor); + + const brightness = calculateBrightness(colorHex); + const foregroundOklch = brightness > 0.5 ? 'oklch(0 0 0)' : 'oklch(1 0 0)'; + root.style.setProperty('--primary-foreground', foregroundOklch); } - - const oklchColor = `oklch(${lightness.toFixed(3)} ${chroma.toFixed(3)} ${hue.toFixed(1)})`; - root.style.setProperty('--primary', oklchColor); - - const brightness = (r * 299 + g * 587 + b * 114) / 1000; - const foregroundOklch = brightness > 0.5 ? 'oklch(0 0 0)' : 'oklch(1 0 0)'; - root.style.setProperty('--primary-foreground', foregroundOklch); } function PreferenceWrapper({ children }: Props) { diff --git a/src/hooks/helpers/useThemeColor.ts b/src/hooks/helpers/useThemeColor.ts index 7a50baa..47a4dae 100644 --- a/src/hooks/helpers/useThemeColor.ts +++ b/src/hooks/helpers/useThemeColor.ts @@ -1,56 +1,13 @@ "use client"; import { useEffect } from "react"; +import { hexToOklch, calculateBrightness } from "@/lib/helpers/colorUtils"; interface UseThemeColorProps { primaryColor?: string | null; enabled?: boolean; } -/** - * تبدیل hex به RGB normalized (0-1) - */ -function hexToRgbNormalized( - hex: string -): { r: number; g: number; b: number } | null { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result - ? { - r: parseInt(result[1], 16) / 255, - g: parseInt(result[2], 16) / 255, - b: parseInt(result[3], 16) / 255, - } - : null; -} - -/** - * تبدیل RGB به OKLCH (تقریبی) - */ -function rgbToOklch(r: number, g: number, b: number): string { - // محاسبه Lightness تقریبی - const lightness = 0.2126 * r + 0.7152 * g + 0.0722 * b; - - // محاسبه Chroma و Hue (ساده شده) - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const chroma = (max - min) * 0.15; // مقدار تقریبی - - let hue = 0; - if (chroma !== 0) { - if (max === r) { - hue = ((g - b) / (max - min) + (g < b ? 6 : 0)) * 60; - } else if (max === g) { - hue = ((b - r) / (max - min) + 2) * 60; - } else { - hue = ((r - g) / (max - min) + 4) * 60; - } - } - - return `oklch(${lightness.toFixed(3)} ${chroma.toFixed(3)} ${hue.toFixed( - 1 - )})`; -} - /** * هوک برای تغییر دینامیک رنگ primary بر اساس رنگ دریافتی از about */ @@ -66,15 +23,14 @@ export function useThemeColor({ // اگر رنگ hex هست، آن را به oklch تبدیل می‌کنیم if (primaryColor.startsWith("#")) { - const rgb = hexToRgbNormalized(primaryColor); - if (rgb) { - const oklchColor = rgbToOklch(rgb.r, rgb.g, rgb.b); - + const oklchColor = hexToOklch(primaryColor); + + if (oklchColor) { // اعمال رنگ primary root.style.setProperty("--primary", oklchColor); // محاسبه رنگ foreground متناسب - const brightness = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000; + const brightness = calculateBrightness(primaryColor); const foregroundOklch = brightness > 0.5 ? "oklch(0 0 0)" : "oklch(1 0 0)"; root.style.setProperty("--primary-foreground", foregroundOklch); diff --git a/src/lib/helpers/colorUtils.ts b/src/lib/helpers/colorUtils.ts new file mode 100644 index 0000000..a915071 --- /dev/null +++ b/src/lib/helpers/colorUtils.ts @@ -0,0 +1,80 @@ +/** + * تبدیل hex به RGB normalized (0-1) + */ +function hexToRgbNormalized(hex: string): { r: number; g: number; b: number } | null { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result + ? { + r: parseInt(result[1], 16) / 255, + g: parseInt(result[2], 16) / 255, + b: parseInt(result[3], 16) / 255, + } + : null; +} + +/** + * تبدیل RGB normalized به oklch + * استفاده از فرمول استاندارد OKLab/OKLCH + */ +function rgbToOklch(r: number, g: number, b: number): string { + try { + // تبدیل sRGB به linear RGB + const toLinear = (c: number) => { + return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); + }; + + const rLinear = toLinear(r); + const gLinear = toLinear(g); + const bLinear = toLinear(b); + + // تبدیل linear RGB به XYZ (D65 white point) + const x = rLinear * 0.4124564 + gLinear * 0.3575761 + bLinear * 0.1804375; + const y = rLinear * 0.2126729 + gLinear * 0.7151522 + bLinear * 0.072175; + const z = rLinear * 0.0193339 + gLinear * 0.119192 + bLinear * 0.9503041; + + // تبدیل XYZ به OKLab + const l = 0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z; + const m = 0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z; + const s = 0.0482003018 * x + 0.2643662691 * y + 0.6338517070 * z; + + const lCbrt = Math.cbrt(l); + const mCbrt = Math.cbrt(m); + const sCbrt = Math.cbrt(s); + + const L = 0.2104542553 * lCbrt + 0.7936177850 * mCbrt - 0.0040720468 * sCbrt; + const a = 1.9779984951 * lCbrt - 2.4285922050 * mCbrt + 0.4505937099 * sCbrt; + const bLab = 0.0259040371 * lCbrt + 0.7827717662 * mCbrt - 0.8086757660 * sCbrt; + + // تبدیل OKLab به OKLCH + const C = Math.sqrt(a * a + bLab * bLab); + let h = Math.atan2(bLab, a) * (180 / Math.PI); + if (h < 0) h += 360; + + // تبدیل L از 0-1 به درصد + const LPercent = L * 100; + + return `oklch(${LPercent.toFixed(2)}% ${C.toFixed(4)} ${h.toFixed(1)})`; + } catch (error) { + // Fallback در صورت خطا + return `oklch(60% 0.15 25)`; + } +} + +/** + * تبدیل hex color به oklch + */ +export function hexToOklch(hex: string): string | null { + const rgb = hexToRgbNormalized(hex); + if (!rgb) return null; + return rgbToOklch(rgb.r, rgb.g, rgb.b); +} + +/** + * محاسبه brightness برای تعیین رنگ foreground + */ +export function calculateBrightness(hex: string): number { + const rgb = hexToRgbNormalized(hex); + if (!rgb) return 0.5; + return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000; +} +