[WEB-5614] chore: custom theme on colour improvement #8356

This commit is contained in:
Anmol Singh Bhatia 2025-12-17 16:54:12 +05:30 committed by GitHub
parent 1f06b67c66
commit 61db42bcb7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 115 additions and 11 deletions

View file

@ -136,3 +136,35 @@ export function isGrayscale(hex: string): boolean {
return false;
}
}
/**
* Calculate relative luminance using WCAG standard
* Returns a value between 0 (black) and 1 (white)
* Based on: https://www.w3.org/TR/WCAG20/#relativeluminancedef
*/
export function getRelativeLuminance(hex: string): number {
try {
const cleanHex = hex.replace("#", "");
const color = chroma(`#${cleanHex}`);
return color.luminance();
} catch (error) {
console.error("Error calculating luminance:", error);
return 0.5; // Safe default
}
}
/**
* Calculate perceptual brightness using weighted RGB formula
* Returns a value between 0 (dark) and 255 (bright)
* Uses ITU-R BT.709 coefficients for better perceptual accuracy
*/
export function getPerceptualBrightness(hex: string): number {
try {
const { r, g, b } = hexToRgb(hex);
// ITU-R BT.709 coefficients
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
} catch (error) {
console.error("Error calculating brightness:", error);
return 128; // Safe default (mid-gray)
}
}

View file

@ -13,7 +13,13 @@ export {
} from "./palette-generator";
// Theme application
export { applyCustomTheme, clearCustomTheme } from "./theme-application";
export {
applyCustomTheme,
clearCustomTheme,
isColorDark,
getOnColorTextColors,
type DarknessDetectionMethod,
} from "./theme-application";
// Color conversion utilities
export {
@ -24,6 +30,8 @@ export {
isGrayscale,
oklchToCSS,
parseOKLCH,
getRelativeLuminance,
getPerceptualBrightness,
// rgbToHex,
type OKLCH,
type RGB,

View file

@ -3,11 +3,79 @@
* Applies generated palettes to CSS variables for Plane's theme system
*/
import { hexToOKLCH, oklchToCSS } from "./color-conversion";
import { hexToOKLCH, oklchToCSS, getRelativeLuminance, getPerceptualBrightness } from "./color-conversion";
import type { OKLCH } from "./color-conversion";
import { ALPHA_MAPPING } from "./constants";
import { generateThemePalettes } from "./palette-generator";
import { getBrandMapping, getNeutralMapping, invertPalette } from "./theme-inversion";
/**
* Color darkness detection methods
*/
export type DarknessDetectionMethod = "wcag" | "oklch" | "perceptual";
/**
* Determine if a color is dark using various methods
*
* Methods:
* - 'wcag': Uses WCAG relative luminance (0-1 scale, threshold 0.5) - Most accurate for accessibility
* - 'oklch': Uses OKLCH lightness (0-1 scale, threshold 0.5) - Good for perceptual uniformity
* - 'perceptual': Uses weighted RGB brightness (0-255 scale, threshold 128) - Simple and fast
*
* @param brandColor - Brand color in hex format
* @param method - Detection method to use (default: 'wcag')
* @returns true if the color is dark, false if light
*/
export function isColorDark(brandColor: string, method: DarknessDetectionMethod = "wcag"): boolean {
switch (method) {
case "wcag": {
// WCAG relative luminance: 0 (black) to 1 (white)
// Threshold of 0.5 means colors darker than 50% gray are considered dark
const luminance = getRelativeLuminance(brandColor);
return luminance < 0.5;
}
case "oklch": {
// OKLCH lightness: 0 (black) to 1 (white)
// Threshold of 0.5 provides good balance for most colors
const oklch = hexToOKLCH(brandColor);
return oklch.l < 0.5;
}
case "perceptual": {
// Perceptual brightness: 0 (black) to 255 (white)
// Threshold of 128 is the midpoint
const brightness = getPerceptualBrightness(brandColor);
return brightness < 128;
}
default:
return getRelativeLuminance(brandColor) < 0.5;
}
}
/**
* Get contrasting text colors for use on a colored background
* Returns white text for dark backgrounds, black text for light backgrounds
*
* @param brandColor - Brand color in hex format
* @param method - Detection method to use (default: 'wcag')
* @returns Object with text and icon colors in OKLCH format
*/
export function getOnColorTextColors(
brandColor: string,
method: DarknessDetectionMethod = "wcag"
): {
textColor: OKLCH;
iconColor: OKLCH;
} {
const isDark = isColorDark(brandColor, method);
const white: OKLCH = { l: 1, c: 0, h: 0 };
const black: OKLCH = { l: 0, c: 0, h: 0 };
return {
textColor: isDark ? white : black,
iconColor: isDark ? black : white,
};
}
/**
* Apply custom theme using 2-color palette system
* Generates full palettes from brand and neutral colors
@ -32,7 +100,6 @@ export function applyCustomTheme(brandColor: string, neutralColor: string, mode:
// Generate palettes directly in OKLCH color space
const { brandPalette, neutralPalette } = generateThemePalettes(brandColor, neutralColor, mode);
const neutralOKLCH = hexToOKLCH(neutralColor);
const brandOKLCH = hexToOKLCH(brandColor);
// For dark mode, invert the palettes
const activeBrandPalette = mode === "dark" ? invertPalette(brandPalette) : brandPalette;
@ -57,14 +124,11 @@ export function applyCustomTheme(brandColor: string, neutralColor: string, mode:
themeElement.style.setProperty(`--color-alpha-black-${key}`, oklchToCSS(neutralOKLCH, value * 100));
});
const isBrandColorDark = brandOKLCH.l < 0.2;
const whiteInOKLCH = { l: 1, c: 0, h: 0 };
const blackInOKLCH = { l: 0, c: 0, h: 0 };
themeElement.style.setProperty(`--text-color-on-color`, oklchToCSS(isBrandColorDark ? whiteInOKLCH : blackInOKLCH));
themeElement.style.setProperty(
`--text-color-icon-on-color`,
oklchToCSS(isBrandColorDark ? blackInOKLCH : whiteInOKLCH)
);
// Apply contrasting text colors for use on colored backgrounds
// Uses WCAG relative luminance for accurate contrast determination
const { textColor, iconColor } = getOnColorTextColors(brandColor, "wcag");
themeElement.style.setProperty(`--text-color-on-color`, oklchToCSS(textColor));
themeElement.style.setProperty(`--text-color-icon-on-color`, oklchToCSS(iconColor));
}
/**