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>
124 lines
4.3 KiB
TypeScript
124 lines
4.3 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
|
|
/**
|
|
* رنگهای ثابت (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 },
|
|
{
|
|
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;
|
|
darkMode: boolean;
|
|
density: 'comfortable' | 'compact';
|
|
brandHue: BrandHue;
|
|
settingsPanelOpen: boolean;
|
|
toggleSidebar: () => void;
|
|
setSidebarOpen: (open: boolean) => void;
|
|
toggleDarkMode: () => void;
|
|
setDensity: (density: 'comfortable' | 'compact') => void;
|
|
setBrandHue: (hue: BrandHue) => void;
|
|
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);
|
|
applyBrand(root, state.brandHue, state.darkMode);
|
|
}
|
|
|
|
export const useUiStore = create<UiState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
sidebarOpen: true,
|
|
darkMode: false,
|
|
density: 'comfortable',
|
|
brandHue: HUES[0],
|
|
settingsPanelOpen: false,
|
|
|
|
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
|
|
setSidebarOpen: (open) => set({ sidebarOpen: open }),
|
|
|
|
toggleDarkMode: () => {
|
|
const next = !get().darkMode;
|
|
set({ darkMode: next });
|
|
applyTheme({ ...get(), darkMode: next });
|
|
},
|
|
|
|
setDensity: (density) => {
|
|
set({ density });
|
|
applyTheme({ ...get(), density });
|
|
},
|
|
|
|
setBrandHue: (brandHue) => {
|
|
set({ brandHue });
|
|
applyTheme({ ...get(), brandHue });
|
|
},
|
|
|
|
toggleSettingsPanel: () => set((s) => ({ settingsPanelOpen: !s.settingsPanelOpen })),
|
|
}),
|
|
{
|
|
name: 'clinicpro-ui',
|
|
onRehydrateStorage: () => (state) => {
|
|
if (state) {
|
|
applyTheme({
|
|
darkMode: state.darkMode ?? false,
|
|
density: state.density ?? 'comfortable',
|
|
brandHue: state.brandHue ?? HUES[0],
|
|
});
|
|
}
|
|
},
|
|
},
|
|
),
|
|
);
|