130 lines
4.3 KiB
TypeScript
130 lines
4.3 KiB
TypeScript
import { ref, onMounted } from 'vue';
|
|
|
|
export type UsePrimaryColorReturn = {
|
|
primaryColor: ReturnType<typeof ref<string>>;
|
|
updatePrimaryColor: (value: string) => void;
|
|
};
|
|
|
|
const STORAGE_KEY = 'primaryColor';
|
|
|
|
function setCookie(name: string, value: string, days = 365) {
|
|
if (typeof document === 'undefined') return;
|
|
const maxAge = days * 24 * 60 * 60;
|
|
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
|
|
}
|
|
|
|
function getStoredPrimaryColor(): string | null {
|
|
if (typeof window === 'undefined') return null;
|
|
return localStorage.getItem(STORAGE_KEY);
|
|
}
|
|
|
|
function parseColorToRGB(color: string): { r: number; g: number; b: number } | null {
|
|
// Supports #rgb, #rrggbb, rgb(), hsl()
|
|
color = color.trim().toLowerCase();
|
|
|
|
// Hex
|
|
if (color.startsWith('#')) {
|
|
let hex = color.slice(1);
|
|
if (hex.length === 3) {
|
|
hex = hex.split('').map((c) => c + c).join('');
|
|
}
|
|
if (hex.length === 6) {
|
|
const num = parseInt(hex, 16);
|
|
return { r: (num >> 16) & 255, g: (num >> 8) & 255, b: num & 255 };
|
|
}
|
|
}
|
|
|
|
// rgb/rgba
|
|
if (color.startsWith('rgb')) {
|
|
const m = color.match(/rgba?\(([^)]+)\)/);
|
|
if (m) {
|
|
const [r, g, b] = m[1]
|
|
.split(',')
|
|
.slice(0, 3)
|
|
.map((v) => parseFloat(v.trim()));
|
|
return { r, g, b };
|
|
}
|
|
}
|
|
|
|
// hsl/hsla
|
|
if (color.startsWith('hsl')) {
|
|
const m = color.match(/hsla?\(([^)]+)\)/);
|
|
if (m) {
|
|
const [hStr, sStr, lStr] = m[1].split(',').map((v) => v.trim());
|
|
const h = parseFloat(hStr);
|
|
const s = parseFloat(sStr) / 100;
|
|
const l = parseFloat(lStr) / 100;
|
|
// convert HSL to RGB
|
|
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
const m2 = l - c / 2;
|
|
let r1 = 0, g1 = 0, b1 = 0;
|
|
if (0 <= h && h < 60) [r1, g1, b1] = [c, x, 0];
|
|
else if (60 <= h && h < 120) [r1, g1, b1] = [x, c, 0];
|
|
else if (120 <= h && h < 180) [r1, g1, b1] = [0, c, x];
|
|
else if (180 <= h && h < 240) [r1, g1, b1] = [0, x, c];
|
|
else if (240 <= h && h < 300) [r1, g1, b1] = [x, 0, c];
|
|
else [r1, g1, b1] = [c, 0, x];
|
|
return {
|
|
r: Math.round((r1 + m2) * 255),
|
|
g: Math.round((g1 + m2) * 255),
|
|
b: Math.round((b1 + m2) * 255),
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getContrastColor(bg: string): '#000000' | '#FFFFFF' {
|
|
const rgb = parseColorToRGB(bg) || { r: 0, g: 0, b: 0 };
|
|
// Relative luminance
|
|
const srgb = [rgb.r, rgb.g, rgb.b].map((v) => {
|
|
const c = v / 255;
|
|
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
});
|
|
const L = 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];
|
|
return L > 0.179 ? '#000000' : '#FFFFFF';
|
|
}
|
|
|
|
export function applyPrimaryColor(value: string): void {
|
|
if (typeof document === 'undefined') return;
|
|
const root = document.documentElement;
|
|
|
|
// Apply to global primary variables used by Tailwind theme tokens
|
|
root.style.setProperty('--primary', value);
|
|
root.style.setProperty('--primary-foreground', getContrastColor(value));
|
|
|
|
// Align commonly used accent variables in user layout to the same main color
|
|
root.style.setProperty('--magenta', value);
|
|
root.style.setProperty('--sidebar-primary', value);
|
|
root.style.setProperty('--sidebar-primary-foreground', getContrastColor(value));
|
|
}
|
|
|
|
export function initializePrimaryColor(): void {
|
|
const saved = getStoredPrimaryColor();
|
|
if (saved) applyPrimaryColor(saved);
|
|
}
|
|
|
|
const primaryColorRef = ref<string>(getStoredPrimaryColor() || '#ff007a');
|
|
|
|
export function usePrimaryColor(): UsePrimaryColorReturn {
|
|
onMounted(() => {
|
|
const saved = getStoredPrimaryColor();
|
|
if (saved) {
|
|
primaryColorRef.value = saved;
|
|
}
|
|
});
|
|
|
|
function updatePrimaryColor(value: string) {
|
|
primaryColorRef.value = value;
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.setItem(STORAGE_KEY, value);
|
|
}
|
|
setCookie(STORAGE_KEY, value);
|
|
applyPrimaryColor(value);
|
|
}
|
|
|
|
return { primaryColor: primaryColorRef, updatePrimaryColor };
|
|
}
|