feat(admin): add orange rgb(241,119,50) as an exact brand-color option
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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',
|
||||
}}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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<UiState, 'darkMode' | 'density' | 'brandHue'>) {
|
||||
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<UiState>()(
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user