From 62e0cdb51888bb2e87d57eee982348752baf694a Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Wed, 15 Jul 2026 17:06:24 +0330 Subject: [PATCH] feat(admin): add orange rgb(241,119,50) as an exact brand-color option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the earlier mis-scoped orange on the personalization panel and instead expose rgb(241,119,50) as a selectable 'رنگ اصلی' swatch. When picked it overrides --primary/-600/-700/-soft/-soft2/-ring inline with exact hex, so every var(--primary) consumer across the whole admin (buttons, active nav, rings) becomes that orange. Non-fixed hues keep the oklch hue pipeline (inline overrides removed on switch). - uiStore: BrandHue interface + optional fixed hex; shared applyBrand() helper handles fixed vs hue, dark-aware soft tints - AdminLayout: reuse applyBrand() (no divergence with store) - Topbar: swatch shows the exact fixed color - tests: fixed-color apply, dark soft, and override-cleanup on switch Co-Authored-By: Claude Opus 4.8 (1M context) --- .../admin/components/layout/AdminLayout.tsx | 5 +- assets/admin/components/layout/Topbar.tsx | 2 +- assets/admin/stores/uiStore.test.ts | 31 +++++++++++ assets/admin/stores/uiStore.ts | 55 +++++++++++++++++-- assets/admin/styles.css | 3 - 5 files changed, 83 insertions(+), 13 deletions(-) diff --git a/assets/admin/components/layout/AdminLayout.tsx b/assets/admin/components/layout/AdminLayout.tsx index 522e692d..c0ae5523 100644 --- a/assets/admin/components/layout/AdminLayout.tsx +++ b/assets/admin/components/layout/AdminLayout.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { Outlet } from 'react-router-dom'; import Sidebar from './Sidebar'; import Topbar from './Topbar'; -import { useUiStore } from '../../stores/uiStore'; +import { useUiStore, applyBrand } from '../../stores/uiStore'; export default function AdminLayout() { const darkMode = useUiStore((s) => s.darkMode); @@ -17,8 +17,7 @@ export default function AdminLayout() { root.classList.toggle('dark', darkMode); root.setAttribute('data-theme', darkMode ? 'dark' : 'light'); root.setAttribute('data-density', density); - root.style.setProperty('--brand-h', String(brandHue.h)); - root.style.setProperty('--brand-c', String(brandHue.c)); + applyBrand(root, brandHue, darkMode); }, [darkMode, density, brandHue]); return ( diff --git a/assets/admin/components/layout/Topbar.tsx b/assets/admin/components/layout/Topbar.tsx index c0dce499..e2c45a3e 100644 --- a/assets/admin/components/layout/Topbar.tsx +++ b/assets/admin/components/layout/Topbar.tsx @@ -124,7 +124,7 @@ export default function Topbar({ onMobileMenuOpen }: { onMobileMenuOpen?: () => title={hue.name} style={{ width: 40, height: 40, borderRadius: 10, - background: `oklch(0.55 ${hue.c} ${hue.h})`, + background: hue.fixed?.primary ?? `oklch(0.55 ${hue.c} ${hue.h})`, border: brandHue.h === hue.h ? '3px solid var(--text)' : '3px solid transparent', display: 'grid', placeItems: 'center', color: '#fff', cursor: 'pointer', }} diff --git a/assets/admin/stores/uiStore.test.ts b/assets/admin/stores/uiStore.test.ts index 5fdde3ea..c2caf2a4 100644 --- a/assets/admin/stores/uiStore.test.ts +++ b/assets/admin/stores/uiStore.test.ts @@ -55,6 +55,37 @@ describe('brandHue', () => { expect(document.documentElement.style.getPropertyValue('--brand-h')).toBe(String(hue.h)); expect(document.documentElement.style.getPropertyValue('--brand-c')).toBe(String(hue.c)); }); + + it('گزینه نارنجی با رنگ ثابت rgb(241,119,50) وجود دارد', () => { + const orange = HUES.find((h) => h.name === 'نارنجی'); + expect(orange?.fixed?.primary).toBe('rgb(241, 119, 50)'); + }); + + it('انتخاب نارنجی، --primary را دقیقاً rgb(241,119,50) و soft روشن را inline می‌کند', () => { + const orange = HUES.find((h) => h.fixed)!; + useUiStore.getState().setBrandHue(orange); + const s = document.documentElement.style; + expect(s.getPropertyValue('--primary')).toBe('rgb(241, 119, 50)'); + expect(s.getPropertyValue('--primary-soft')).toBe(orange.fixed!.soft); + expect(s.getPropertyValue('--ring')).toBe(orange.fixed!.ring); + }); + + it('در حالت تیره، soft تیره اعمال می‌شود', () => { + const orange = HUES.find((h) => h.fixed)!; + useUiStore.getState().toggleDarkMode(); // dark + useUiStore.getState().setBrandHue(orange); + expect(document.documentElement.style.getPropertyValue('--primary-soft')) + .toBe(orange.fixed!.softDark); + }); + + it('برگشت به hue بدون رنگ ثابت، override اینلاین --primary را پاک می‌کند', () => { + const orange = HUES.find((h) => h.fixed)!; + useUiStore.getState().setBrandHue(orange); + expect(document.documentElement.style.getPropertyValue('--primary')).not.toBe(''); + useUiStore.getState().setBrandHue(HUES[0]); // بنفش، بدون fixed + expect(document.documentElement.style.getPropertyValue('--primary')).toBe(''); + expect(document.documentElement.style.getPropertyValue('--primary-soft')).toBe(''); + }); }); describe('settingsPanel', () => { diff --git a/assets/admin/stores/uiStore.ts b/assets/admin/stores/uiStore.ts index 8da9d5b5..f387dad0 100644 --- a/assets/admin/stores/uiStore.ts +++ b/assets/admin/stores/uiStore.ts @@ -1,15 +1,33 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; -export const HUES = [ +/** + * رنگ‌های ثابت (hex) برای گزینه‌هایی که باید دقیقاً یک رنگ مشخص باشند و نه + * محاسبه‌شده از hue (فرمول oklch با L ثابت نمی‌تواند رنگ روشن دقیق بدهد). + */ +export interface BrandFixed { + primary: string; p600: string; p700: string; + soft: string; soft2: string; softDark: string; soft2Dark: string; ring: string; +} +export interface BrandHue { name: string; h: number; c: number; fixed?: BrandFixed; } + +export const HUES: BrandHue[] = [ { name: 'بنفش', h: 277, c: 0.177 }, { name: 'آبی پزشکی', h: 256, c: 0.15 }, { name: 'فیروزه‌ای', h: 205, c: 0.12 }, { name: 'سبز سلامت', h: 162, c: 0.13 }, { name: 'نیلی', h: 272, c: 0.16 }, -] as const; - -export type BrandHue = (typeof HUES)[number]; + { + name: 'نارنجی', h: 45, c: 0.16, + // رنگ اختصاصی rgb(241,119,50) — با انتخابش رنگ اصلی کل سایت دقیقاً همین می‌شود + fixed: { + primary: 'rgb(241, 119, 50)', p600: '#db5a1f', p700: '#b74d16', + soft: '#fdeee4', soft2: '#fbd9c4', + softDark: '#3a2b23', soft2Dark: '#4a3527', + ring: 'rgba(241, 119, 50, 0.32)', + }, + }, +]; interface UiState { sidebarOpen: boolean; @@ -25,13 +43,38 @@ interface UiState { toggleSettingsPanel: () => void; } +const FIXED_VAR_KEYS = [ + '--primary', '--primary-600', '--primary-700', + '--primary-soft', '--primary-soft2', '--ring', +]; + +/** + * اعمال رنگ برند روی ریشه. اگر hue رنگ ثابت داشته باشد، مقادیر hex را مستقیماً + * inline می‌گذارد (روی oklch/base غالب می‌شود)؛ وگرنه override را پاک می‌کند تا + * فرمول hue‌محورِ stylesheet دوباره فعال شود. hue/chroma همیشه ست می‌شوند. + */ +export function applyBrand(root: HTMLElement, brandHue: BrandHue, darkMode: boolean) { + root.style.setProperty('--brand-h', String(brandHue.h)); + root.style.setProperty('--brand-c', String(brandHue.c)); + const f = brandHue.fixed; + if (f) { + root.style.setProperty('--primary', f.primary); + root.style.setProperty('--primary-600', f.p600); + root.style.setProperty('--primary-700', f.p700); + root.style.setProperty('--primary-soft', darkMode ? f.softDark : f.soft); + root.style.setProperty('--primary-soft2', darkMode ? f.soft2Dark : f.soft2); + root.style.setProperty('--ring', f.ring); + } else { + FIXED_VAR_KEYS.forEach((k) => root.style.removeProperty(k)); + } +} + function applyTheme(state: Pick) { const root = document.documentElement; root.classList.toggle('dark', state.darkMode); root.setAttribute('data-theme', state.darkMode ? 'dark' : 'light'); root.setAttribute('data-density', state.density); - root.style.setProperty('--brand-h', String(state.brandHue.h)); - root.style.setProperty('--brand-c', String(state.brandHue.c)); + applyBrand(root, state.brandHue, state.darkMode); } export const useUiStore = create()( diff --git a/assets/admin/styles.css b/assets/admin/styles.css index 997ce3da..c1bdc6e2 100644 --- a/assets/admin/styles.css +++ b/assets/admin/styles.css @@ -658,9 +658,6 @@ table.t tbody tr:last-child td { border-bottom: none; } animation: slidein .3s var(--ease); border-radius: 0; max-height: 100%; } -/* رنگ اختصاصی بخش «شخصی‌سازی» — فقط همین پنل، نه جای دیگر پنل */ -.settings-panel .modal-head h2 { color: rgb(241, 119, 50); } -.settings-panel .seg button.on { background: rgb(241, 119, 50); color: #fff; box-shadow: var(--shadow-sm); } /* ── Form rows ───────────────────────────────────────────────── */ .form-row { display: flex; flex-direction: column; gap: 7px; margin-bottom: 16px; }