Add dynamic sidebar text color with brand color contrast
- Add sidebar text color picker to Branding Settings page - Implement auto-calculated complementary text colors based on brand color luminance - Dark themes get light tinted text, light themes get dark tinted text - Add navigation preview showing text on gradient background - Support 10 new lighter color palettes (Soft Mint, Lavender, Peach, etc.) - Add CSS utility classes for brand-text with opacity support - Update sidebar and navigation components to use dynamic text colors - Add sidebar_text_color field to Tenant model with migration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,80 @@
|
||||
* Color utility functions for generating brand color palettes
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert hex color to RGB values
|
||||
*/
|
||||
export function hexToRGB(hex: string): { r: number; g: number; b: number } {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
if (!result) return { r: 0, g: 0, b: 0 };
|
||||
return {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex color to RGB string for Tailwind CSS variable opacity support
|
||||
* Returns "R G B" format (space-separated) for use with rgb() and opacity
|
||||
*/
|
||||
export function hexToRGBString(hex: string): string {
|
||||
const { r, g, b } = hexToRGB(hex);
|
||||
return `${r} ${g} ${b}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate relative luminance of a color (WCAG formula)
|
||||
* Returns value between 0 (black) and 1 (white)
|
||||
*/
|
||||
export function getLuminance(hex: string): number {
|
||||
const { r, g, b } = hexToRGB(hex);
|
||||
const sR = r / 255;
|
||||
const sG = g / 255;
|
||||
const sB = b / 255;
|
||||
const R = sR <= 0.03928 ? sR / 12.92 : Math.pow((sR + 0.055) / 1.055, 2.4);
|
||||
const G = sG <= 0.03928 ? sG / 12.92 : Math.pow((sG + 0.055) / 1.055, 2.4);
|
||||
const B = sB <= 0.03928 ? sB / 12.92 : Math.pow((sB + 0.055) / 1.055, 2.4);
|
||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a dark complementary text color from a brand color
|
||||
* Creates a very dark shade of the brand's hue for use on light backgrounds
|
||||
*/
|
||||
export function getDarkBrandTextColor(brandColor: string): string {
|
||||
const { h, s } = hexToHSL(brandColor);
|
||||
// Create a very dark, moderately saturated version of the brand color
|
||||
// Lightness ~12% for strong contrast, saturation capped for readability
|
||||
return hslToHex(h, Math.min(s, 50), 12);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a light complementary text color from a brand color
|
||||
* Creates a very light shade of the brand's hue for use on dark backgrounds
|
||||
*/
|
||||
export function getLightBrandTextColor(brandColor: string): string {
|
||||
const { h, s } = hexToHSL(brandColor);
|
||||
// Create a very light, soft version of the brand color
|
||||
// Lightness ~92% for good contrast, lower saturation for softness
|
||||
return hslToHex(h, Math.min(s, 40), 92);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate text color for optimal contrast on a background
|
||||
* Uses WCAG 2.1 contrast ratio guidelines
|
||||
* Returns complementary brand shades instead of pure black/white
|
||||
*/
|
||||
export function getContrastTextColor(backgroundColor: string, brandColor?: string): string {
|
||||
const luminance = getLuminance(backgroundColor);
|
||||
if (luminance > 0.179) {
|
||||
// Light background - use dark brand shade
|
||||
return brandColor ? getDarkBrandTextColor(brandColor) : '#1f2937';
|
||||
}
|
||||
// Dark background - use light brand shade
|
||||
return brandColor ? getLightBrandTextColor(brandColor) : '#ffffff';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex color to HSL values
|
||||
*/
|
||||
@@ -94,15 +168,34 @@ export function applyColorPalette(palette: Record<string, string>): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply primary and secondary colors including the secondary color variable
|
||||
* Apply primary and secondary colors including contrast text colors
|
||||
* @param primaryColor - The main brand color
|
||||
* @param secondaryColor - Optional secondary color for gradients
|
||||
* @param sidebarTextColor - Optional custom sidebar text color (auto-calculated if not provided)
|
||||
*/
|
||||
export function applyBrandColors(primaryColor: string, secondaryColor?: string): void {
|
||||
export function applyBrandColors(primaryColor: string, secondaryColor?: string, sidebarTextColor?: string): void {
|
||||
const palette = generateColorPalette(primaryColor);
|
||||
applyColorPalette(palette);
|
||||
|
||||
// Set the secondary color variable (used for gradients)
|
||||
const root = document.documentElement;
|
||||
|
||||
// Set the secondary color variable (used for gradients)
|
||||
root.style.setProperty('--color-brand-secondary', secondaryColor || primaryColor);
|
||||
|
||||
// Set contrast text colors for each shade level
|
||||
// These ensure readable text on brand-colored backgrounds
|
||||
// Pass primaryColor so dark text is a complementary shade, not generic black
|
||||
Object.entries(palette).forEach(([shade, color]) => {
|
||||
const textColor = getContrastTextColor(color, primaryColor);
|
||||
root.style.setProperty(`--color-brand-${shade}-text`, textColor);
|
||||
});
|
||||
|
||||
// Set main brand text color (for use with brand-600 backgrounds like sidebar)
|
||||
// Use custom sidebarTextColor if provided, otherwise auto-calculate based on contrast
|
||||
const brandTextColor = sidebarTextColor || getContrastTextColor(palette['600'], primaryColor);
|
||||
root.style.setProperty('--color-brand-text', brandTextColor);
|
||||
// Also set RGB version for Tailwind opacity support (e.g., text-brand-text/70)
|
||||
root.style.setProperty('--color-brand-text-rgb', hexToRGBString(brandTextColor));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user