[WEB-5614] chore: custom theme on colour improvement #8356
This commit is contained in:
parent
1f06b67c66
commit
61db42bcb7
3 changed files with 115 additions and 11 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue