From 7fb4aa6ac7f58ceaed29c1784364e7bf85bc4be3 Mon Sep 17 00:00:00 2001 From: David Klakurka Date: Sat, 29 Nov 2025 00:08:16 -0800 Subject: [PATCH 1/4] Implemented an effective 'dark mode' when tertiary color is too light --- react/lib/components/Widget/Widget.tsx | 33 ++-- react/lib/util/color.ts | 227 +++++++++++++++++++++++++ react/lib/util/index.ts | 1 + 3 files changed, 247 insertions(+), 14 deletions(-) create mode 100644 react/lib/util/color.ts diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index ea71a9d5..2b20ffc7 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -41,6 +41,7 @@ import { CryptoCurrency, DEFAULT_DONATION_RATE, DEFAULT_MINIMUM_DONATION_AMOUNT, + isLightColor, } from '../../util'; import AltpaymentWidget from './AltpaymentWidget' import { @@ -322,6 +323,7 @@ export const Widget: React.FunctionComponent = props => { const [isAboveMinimumAltpaymentAmount, setIsAboveMinimumAltpaymentAmount] = useState(null) const theme = useTheme(props.theme, isValidXecAddress(to)) + const isLightTertiary = useMemo(() => isLightColor(theme.palette.tertiary), [theme.palette.tertiary]) const [thisAmount, setThisAmount] = useState(props.amount) const [thisCurrencyObject, setThisCurrencyObject] = useState(props.currencyObject) @@ -353,13 +355,13 @@ export const Widget: React.FunctionComponent = props => { return { root: { minWidth: '240px', - background: '#f5f5f7', + background: isLightTertiary ? '#2a2a2a' : '#f5f5f7', position: 'relative', overflow: 'hidden', }, qrCode: { - background: '#fff', - border: '1px solid #eee', + background: isLightTertiary ? '#1a1a1a' : '#fff', + border: isLightTertiary ? '1px solid #333' : '1px solid #eee', borderRadius: '4px', outline: 'none', lineHeight: 0, @@ -381,18 +383,19 @@ export const Widget: React.FunctionComponent = props => { }, copyTextContainer: { display: base.loading ? 'none' : 'block', - background: '#ffffffcc', + background: isLightTertiary ? '#1a1a1acc' : '#ffffffcc', padding: '0 0.15rem 0.15rem 0', }, copyText: { lineHeight: '1.2em', fontSize: '0.7em', color: base.theme.palette.tertiary, - textShadow: - '#fff -2px 0 1px, #fff 0 -2px 1px, #fff 0 2px 1px, #fff 2px 0 1px', + textShadow: isLightTertiary + ? '#000 -2px 0 1px, #000 0 -2px 1px, #000 0 2px 1px, #000 2px 0 1px' + : '#fff -2px 0 1px, #fff 0 -2px 1px, #fff 0 2px 1px, #fff 2px 0 1px', '&:disabled span': { filter: 'blur(2px)', - color: 'rgba(0, 0, 0, 0.5)', + color: isLightTertiary ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)', }, }, text: { @@ -404,7 +407,7 @@ export const Widget: React.FunctionComponent = props => { }, footer: { fontSize: '0.6rem', - color: '#a8a8a8', + color: isLightTertiary ? '#888888' : '#a8a8a8', fontWeight: 'normal', userSelect: 'none', display: 'flex', @@ -424,7 +427,8 @@ export const Widget: React.FunctionComponent = props => { cursor: 'pointer', padding: '6px 12px', marginTop: '20px', - background: '#e9e9e9', + background: isLightTertiary ? '#444444' : '#e9e9e9', + color: isLightTertiary ? '#ffffff' : 'inherit', borderRadius: '5px', transition: 'all ease-in-out 200ms', opacity: 0, @@ -477,7 +481,7 @@ export const Widget: React.FunctionComponent = props => { animationDelay: '0.4s', }, } - }, [success, qrLoading, theme, recentlyCopied, copied]) + }, [success, qrLoading, theme, recentlyCopied, copied, isLightTertiary]) const bchSvg = useMemo((): string => { const color = theme.palette.logo ?? theme.palette.primary @@ -1031,6 +1035,7 @@ export const Widget: React.FunctionComponent = props => { size={300} level="H" value={url} + bgColor={isLightTertiary ? '#1a1a1a' : '#ffffff'} fgColor={theme.palette.tertiary as unknown as string} imageSettings={{ src: success ? checkSvg : isValidCashAddress(to) ? bchSvg : xecSvg, @@ -1059,7 +1064,7 @@ export const Widget: React.FunctionComponent = props => { @@ -1342,7 +1347,7 @@ export const Widget: React.FunctionComponent = props => { width: '13px', height: '13px', fill: donationEnabled ? '#f44336' : 'none', - stroke: donationEnabled ? '#f44336' : '#5c5c5c', + stroke: donationEnabled ? '#f44336' : (isLightTertiary ? '#a0a0a0' : '#5c5c5c'), strokeWidth: donationEnabled ? 0 : 1.5, transition: 'all 0.2s ease-in-out', '&:hover': { @@ -1380,7 +1385,7 @@ export const Widget: React.FunctionComponent = props => { padding: '0px 2px 0px 4px', fontSize: '0.6rem', textAlign: 'left', - color: '#5c5c5c', + color: isLightTertiary ? '#b0b0b0' : '#5c5c5c', lineHeight: '1.5em', }, '& fieldset': { @@ -1393,7 +1398,7 @@ export const Widget: React.FunctionComponent = props => { component="span" sx={{ fontSize: '0.6rem', - color: '#5c5c5c', + color: isLightTertiary ? '#b0b0b0' : '#5c5c5c', flexShrink: 0, marginLeft: '2px', }} diff --git a/react/lib/util/color.ts b/react/lib/util/color.ts new file mode 100644 index 00000000..18a8ad92 --- /dev/null +++ b/react/lib/util/color.ts @@ -0,0 +1,227 @@ +/** + * Map of common CSS color names to their hex values + * Focused on colors that are relevant for light/dark detection + */ +const colorNameToHex: Record = { + white: '#ffffff', + black: '#000000', + silver: '#c0c0c0', + gray: '#808080', + grey: '#808080', + whitesmoke: '#f5f5f5', + snow: '#fffafa', + ghostwhite: '#f8f8ff', + floralwhite: '#fffaf0', + ivory: '#fffff0', + beige: '#f5f5dc', + azure: '#f0ffff', + aliceblue: '#f0f8ff', + mintcream: '#f5fffa', + honeydew: '#f0fff0', + seashell: '#fff5ee', + linen: '#faf0e6', + oldlace: '#fdf5e6', + lavenderblush: '#fff0f5', + mistyrose: '#ffe4e1', + gainsboro: '#dcdcdc', + lightgray: '#d3d3d3', + lightgrey: '#d3d3d3', + darkgray: '#a9a9a9', + darkgrey: '#a9a9a9', + dimgray: '#696969', + dimgrey: '#696969', + lightslategray: '#778899', + lightslategrey: '#778899', + slategray: '#708090', + slategrey: '#708090', + darkslategray: '#2f4f4f', + darkslategrey: '#2f4f4f', + yellow: '#ffff00', + lightyellow: '#ffffe0', + lemonchiffon: '#fffacd', + lightgoldenrodyellow: '#fafad2', + papayawhip: '#ffefd5', + moccasin: '#ffe4b5', + peachpuff: '#ffdab9', + palegoldenrod: '#eee8aa', + khaki: '#f0e68c', + cornsilk: '#fff8dc', + blanchedalmond: '#ffebcd', + bisque: '#ffe4c4', + navajowhite: '#ffdead', + wheat: '#f5deb3', + antiquewhite: '#faebd7', + lightcyan: '#e0ffff', + cyan: '#00ffff', + aqua: '#00ffff', + aquamarine: '#7fffd4', + paleturquoise: '#afeeee', + lightblue: '#add8e6', + powderblue: '#b0e0e6', + lightsteelblue: '#b0c4de', + lightskyblue: '#87cefa', + skyblue: '#87ceeb', + lavender: '#e6e6fa', + plum: '#dda0dd', + thistle: '#d8bfd8', + pink: '#ffc0cb', + lightpink: '#ffb6c1', + palevioletred: '#db7093', + lightcoral: '#f08080', + lightsalmon: '#ffa07a', + lightgreen: '#90ee90', + palegreen: '#98fb98', + lime: '#00ff00', + limegreen: '#32cd32', + springgreen: '#00ff7f', + mediumspringgreen: '#00fa9a', + greenyellow: '#adff2f', + chartreuse: '#7fff00', + lawngreen: '#7cfc00', + maroon: '#800000', + darkred: '#8b0000', + red: '#ff0000', + brown: '#a52a2a', + firebrick: '#b22222', + indianred: '#cd5c5c', + crimson: '#dc143c', + tomato: '#ff6347', + orangered: '#ff4500', + coral: '#ff7f50', + darkorange: '#ff8c00', + orange: '#ffa500', + gold: '#ffd700', + goldenrod: '#daa520', + darkgoldenrod: '#b8860b', + peru: '#cd853f', + chocolate: '#d2691e', + saddlebrown: '#8b4513', + sienna: '#a0522d', + burlywood: '#deb887', + tan: '#d2b48c', + rosybrown: '#bc8f8f', + sandybrown: '#f4a460', + salmon: '#fa8072', + darksalmon: '#e9967a', + purple: '#800080', + darkmagenta: '#8b008b', + fuchsia: '#ff00ff', + magenta: '#ff00ff', + mediumorchid: '#ba55d3', + mediumpurple: '#9370db', + blueviolet: '#8a2be2', + darkviolet: '#9400d3', + darkorchid: '#9932cc', + violet: '#ee82ee', + orchid: '#da70d6', + hotpink: '#ff69b4', + deeppink: '#ff1493', + rebeccapurple: '#663399', + indigo: '#4b0082', + slateblue: '#6a5acd', + darkslateblue: '#483d8b', + mediumslateblue: '#7b68ee', + navy: '#000080', + darkblue: '#00008b', + mediumblue: '#0000cd', + blue: '#0000ff', + royalblue: '#4169e1', + cornflowerblue: '#6495ed', + dodgerblue: '#1e90ff', + deepskyblue: '#00bfff', + cadetblue: '#5f9ea0', + steelblue: '#4682b4', + teal: '#008080', + darkcyan: '#008b8b', + darkturquoise: '#00ced1', + mediumturquoise: '#48d1cc', + turquoise: '#40e0d0', + mediumaquamarine: '#66cdaa', + green: '#008000', + darkgreen: '#006400', + seagreen: '#2e8b57', + mediumseagreen: '#3cb371', + forestgreen: '#228b22', + olive: '#808000', + darkolivegreen: '#556b2f', + olivedrab: '#6b8e23', + darkseagreen: '#8fbc8f', + yellowgreen: '#9acd32', +}; + +/** + * Converts a hex color to RGB values + */ +export const hexToRgb = (hex: string): { r: number; g: number; b: number } | null => { + // Remove # if present + const cleanHex = hex.replace(/^#/, ''); + + // Handle 3-character hex codes + const fullHex = cleanHex.length === 3 + ? cleanHex.split('').map(c => c + c).join('') + : cleanHex; + + const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(fullHex); + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + } + : null; +}; + +/** + * Normalizes a color value to a hex code + * Handles hex codes (with or without #) and named CSS colors + */ +export const normalizeColorToHex = (color: string): string | null => { + if (!color) return null; + + const trimmed = color.trim().toLowerCase(); + + // Check if it's a named color + if (colorNameToHex[trimmed]) { + return colorNameToHex[trimmed]; + } + + // Otherwise treat as hex + return trimmed.startsWith('#') ? trimmed : `#${trimmed}`; +}; + +/** + * Calculates the relative luminance of a color + * Based on WCAG 2.0 formula: https://www.w3.org/TR/WCAG20/#relativeluminancedef + * Returns a value between 0 (black) and 1 (white) + */ +export const getLuminance = (color: string): number => { + const hex = normalizeColorToHex(color); + if (!hex) return 0; + + const rgb = hexToRgb(hex); + if (!rgb) return 0; + + const { r, g, b } = rgb; + + // Convert to sRGB + const sR = r / 255; + const sG = g / 255; + const sB = b / 255; + + // Apply gamma correction + 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); + + // Calculate luminance + return 0.2126 * R + 0.7152 * G + 0.0722 * B; +}; + +/** + * Determines if a color is considered "light" + * A color is considered light if its luminance is greater than 0.5 + * This threshold can be adjusted based on needs + */ +export const isLightColor = (color: string, threshold = 0.5): boolean => { + return getLuminance(color) > threshold; +}; diff --git a/react/lib/util/index.ts b/react/lib/util/index.ts index 5287f47e..06f0b895 100644 --- a/react/lib/util/index.ts +++ b/react/lib/util/index.ts @@ -1,6 +1,7 @@ export * from './address'; export * from './api-client'; export * from './cashtab'; +export * from './color'; export * from './constants'; export * from './format'; export * from './opReturn'; From 2ada570f0815263f1e3317056c60b71b59e4b9c5 Mon Sep 17 00:00:00 2001 From: David Klakurka Date: Sat, 29 Nov 2025 10:16:44 -0800 Subject: [PATCH 2/4] Changed dark mode variable naming. --- react/lib/components/Widget/Widget.tsx | 34 ++--- react/lib/util/color.ts | 176 ++----------------------- 2 files changed, 30 insertions(+), 180 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 2b20ffc7..7037b5a9 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -41,7 +41,7 @@ import { CryptoCurrency, DEFAULT_DONATION_RATE, DEFAULT_MINIMUM_DONATION_AMOUNT, - isLightColor, + darkMode, } from '../../util'; import AltpaymentWidget from './AltpaymentWidget' import { @@ -323,7 +323,7 @@ export const Widget: React.FunctionComponent = props => { const [isAboveMinimumAltpaymentAmount, setIsAboveMinimumAltpaymentAmount] = useState(null) const theme = useTheme(props.theme, isValidXecAddress(to)) - const isLightTertiary = useMemo(() => isLightColor(theme.palette.tertiary), [theme.palette.tertiary]) + const isDarkMode = useMemo(() => darkMode(theme.palette.tertiary), [theme.palette.tertiary]) const [thisAmount, setThisAmount] = useState(props.amount) const [thisCurrencyObject, setThisCurrencyObject] = useState(props.currencyObject) @@ -355,13 +355,13 @@ export const Widget: React.FunctionComponent = props => { return { root: { minWidth: '240px', - background: isLightTertiary ? '#2a2a2a' : '#f5f5f7', + background: isDarkMode ? '#2a2a2a' : '#f5f5f7', position: 'relative', overflow: 'hidden', }, qrCode: { - background: isLightTertiary ? '#1a1a1a' : '#fff', - border: isLightTertiary ? '1px solid #333' : '1px solid #eee', + background: isDarkMode ? '#1a1a1a' : '#fff', + border: isDarkMode ? '1px solid #333' : '1px solid #eee', borderRadius: '4px', outline: 'none', lineHeight: 0, @@ -383,19 +383,19 @@ export const Widget: React.FunctionComponent = props => { }, copyTextContainer: { display: base.loading ? 'none' : 'block', - background: isLightTertiary ? '#1a1a1acc' : '#ffffffcc', + background: isDarkMode ? '#1a1a1acc' : '#ffffffcc', padding: '0 0.15rem 0.15rem 0', }, copyText: { lineHeight: '1.2em', fontSize: '0.7em', color: base.theme.palette.tertiary, - textShadow: isLightTertiary + textShadow: isDarkMode ? '#000 -2px 0 1px, #000 0 -2px 1px, #000 0 2px 1px, #000 2px 0 1px' : '#fff -2px 0 1px, #fff 0 -2px 1px, #fff 0 2px 1px, #fff 2px 0 1px', '&:disabled span': { filter: 'blur(2px)', - color: isLightTertiary ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)', + color: isDarkMode ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)', }, }, text: { @@ -407,7 +407,7 @@ export const Widget: React.FunctionComponent = props => { }, footer: { fontSize: '0.6rem', - color: isLightTertiary ? '#888888' : '#a8a8a8', + color: isDarkMode ? '#888888' : '#a8a8a8', fontWeight: 'normal', userSelect: 'none', display: 'flex', @@ -427,8 +427,8 @@ export const Widget: React.FunctionComponent = props => { cursor: 'pointer', padding: '6px 12px', marginTop: '20px', - background: isLightTertiary ? '#444444' : '#e9e9e9', - color: isLightTertiary ? '#ffffff' : 'inherit', + background: isDarkMode ? '#444444' : '#e9e9e9', + color: isDarkMode ? '#ffffff' : 'inherit', borderRadius: '5px', transition: 'all ease-in-out 200ms', opacity: 0, @@ -481,7 +481,7 @@ export const Widget: React.FunctionComponent = props => { animationDelay: '0.4s', }, } - }, [success, qrLoading, theme, recentlyCopied, copied, isLightTertiary]) + }, [success, qrLoading, theme, recentlyCopied, copied, isDarkMode]) const bchSvg = useMemo((): string => { const color = theme.palette.logo ?? theme.palette.primary @@ -1035,7 +1035,7 @@ export const Widget: React.FunctionComponent = props => { size={300} level="H" value={url} - bgColor={isLightTertiary ? '#1a1a1a' : '#ffffff'} + bgColor={isDarkMode ? '#1a1a1a' : '#ffffff'} fgColor={theme.palette.tertiary as unknown as string} imageSettings={{ src: success ? checkSvg : isValidCashAddress(to) ? bchSvg : xecSvg, @@ -1064,7 +1064,7 @@ export const Widget: React.FunctionComponent = props => { @@ -1347,7 +1347,7 @@ export const Widget: React.FunctionComponent = props => { width: '13px', height: '13px', fill: donationEnabled ? '#f44336' : 'none', - stroke: donationEnabled ? '#f44336' : (isLightTertiary ? '#a0a0a0' : '#5c5c5c'), + stroke: donationEnabled ? '#f44336' : (isDarkMode ? '#a0a0a0' : '#5c5c5c'), strokeWidth: donationEnabled ? 0 : 1.5, transition: 'all 0.2s ease-in-out', '&:hover': { @@ -1385,7 +1385,7 @@ export const Widget: React.FunctionComponent = props => { padding: '0px 2px 0px 4px', fontSize: '0.6rem', textAlign: 'left', - color: isLightTertiary ? '#b0b0b0' : '#5c5c5c', + color: isDarkMode ? '#b0b0b0' : '#5c5c5c', lineHeight: '1.5em', }, '& fieldset': { @@ -1398,7 +1398,7 @@ export const Widget: React.FunctionComponent = props => { component="span" sx={{ fontSize: '0.6rem', - color: isLightTertiary ? '#b0b0b0' : '#5c5c5c', + color: isDarkMode ? '#b0b0b0' : '#5c5c5c', flexShrink: 0, marginLeft: '2px', }} diff --git a/react/lib/util/color.ts b/react/lib/util/color.ts index 18a8ad92..6a173cf0 100644 --- a/react/lib/util/color.ts +++ b/react/lib/util/color.ts @@ -1,154 +1,3 @@ -/** - * Map of common CSS color names to their hex values - * Focused on colors that are relevant for light/dark detection - */ -const colorNameToHex: Record = { - white: '#ffffff', - black: '#000000', - silver: '#c0c0c0', - gray: '#808080', - grey: '#808080', - whitesmoke: '#f5f5f5', - snow: '#fffafa', - ghostwhite: '#f8f8ff', - floralwhite: '#fffaf0', - ivory: '#fffff0', - beige: '#f5f5dc', - azure: '#f0ffff', - aliceblue: '#f0f8ff', - mintcream: '#f5fffa', - honeydew: '#f0fff0', - seashell: '#fff5ee', - linen: '#faf0e6', - oldlace: '#fdf5e6', - lavenderblush: '#fff0f5', - mistyrose: '#ffe4e1', - gainsboro: '#dcdcdc', - lightgray: '#d3d3d3', - lightgrey: '#d3d3d3', - darkgray: '#a9a9a9', - darkgrey: '#a9a9a9', - dimgray: '#696969', - dimgrey: '#696969', - lightslategray: '#778899', - lightslategrey: '#778899', - slategray: '#708090', - slategrey: '#708090', - darkslategray: '#2f4f4f', - darkslategrey: '#2f4f4f', - yellow: '#ffff00', - lightyellow: '#ffffe0', - lemonchiffon: '#fffacd', - lightgoldenrodyellow: '#fafad2', - papayawhip: '#ffefd5', - moccasin: '#ffe4b5', - peachpuff: '#ffdab9', - palegoldenrod: '#eee8aa', - khaki: '#f0e68c', - cornsilk: '#fff8dc', - blanchedalmond: '#ffebcd', - bisque: '#ffe4c4', - navajowhite: '#ffdead', - wheat: '#f5deb3', - antiquewhite: '#faebd7', - lightcyan: '#e0ffff', - cyan: '#00ffff', - aqua: '#00ffff', - aquamarine: '#7fffd4', - paleturquoise: '#afeeee', - lightblue: '#add8e6', - powderblue: '#b0e0e6', - lightsteelblue: '#b0c4de', - lightskyblue: '#87cefa', - skyblue: '#87ceeb', - lavender: '#e6e6fa', - plum: '#dda0dd', - thistle: '#d8bfd8', - pink: '#ffc0cb', - lightpink: '#ffb6c1', - palevioletred: '#db7093', - lightcoral: '#f08080', - lightsalmon: '#ffa07a', - lightgreen: '#90ee90', - palegreen: '#98fb98', - lime: '#00ff00', - limegreen: '#32cd32', - springgreen: '#00ff7f', - mediumspringgreen: '#00fa9a', - greenyellow: '#adff2f', - chartreuse: '#7fff00', - lawngreen: '#7cfc00', - maroon: '#800000', - darkred: '#8b0000', - red: '#ff0000', - brown: '#a52a2a', - firebrick: '#b22222', - indianred: '#cd5c5c', - crimson: '#dc143c', - tomato: '#ff6347', - orangered: '#ff4500', - coral: '#ff7f50', - darkorange: '#ff8c00', - orange: '#ffa500', - gold: '#ffd700', - goldenrod: '#daa520', - darkgoldenrod: '#b8860b', - peru: '#cd853f', - chocolate: '#d2691e', - saddlebrown: '#8b4513', - sienna: '#a0522d', - burlywood: '#deb887', - tan: '#d2b48c', - rosybrown: '#bc8f8f', - sandybrown: '#f4a460', - salmon: '#fa8072', - darksalmon: '#e9967a', - purple: '#800080', - darkmagenta: '#8b008b', - fuchsia: '#ff00ff', - magenta: '#ff00ff', - mediumorchid: '#ba55d3', - mediumpurple: '#9370db', - blueviolet: '#8a2be2', - darkviolet: '#9400d3', - darkorchid: '#9932cc', - violet: '#ee82ee', - orchid: '#da70d6', - hotpink: '#ff69b4', - deeppink: '#ff1493', - rebeccapurple: '#663399', - indigo: '#4b0082', - slateblue: '#6a5acd', - darkslateblue: '#483d8b', - mediumslateblue: '#7b68ee', - navy: '#000080', - darkblue: '#00008b', - mediumblue: '#0000cd', - blue: '#0000ff', - royalblue: '#4169e1', - cornflowerblue: '#6495ed', - dodgerblue: '#1e90ff', - deepskyblue: '#00bfff', - cadetblue: '#5f9ea0', - steelblue: '#4682b4', - teal: '#008080', - darkcyan: '#008b8b', - darkturquoise: '#00ced1', - mediumturquoise: '#48d1cc', - turquoise: '#40e0d0', - mediumaquamarine: '#66cdaa', - green: '#008000', - darkgreen: '#006400', - seagreen: '#2e8b57', - mediumseagreen: '#3cb371', - forestgreen: '#228b22', - olive: '#808000', - darkolivegreen: '#556b2f', - olivedrab: '#6b8e23', - darkseagreen: '#8fbc8f', - yellowgreen: '#9acd32', -}; - /** * Converts a hex color to RGB values */ @@ -172,21 +21,22 @@ export const hexToRgb = (hex: string): { r: number; g: number; b: number } | nul }; /** - * Normalizes a color value to a hex code - * Handles hex codes (with or without #) and named CSS colors + * Normalizes any CSS color value to a hex code using the browser's canvas API + * Handles hex codes, named colors (e.g., "navy"), rgb(), hsl(), etc. */ export const normalizeColorToHex = (color: string): string | null => { if (!color) return null; - const trimmed = color.trim().toLowerCase(); + const trimmed = color.trim(); + if (!trimmed) return null; - // Check if it's a named color - if (colorNameToHex[trimmed]) { - return colorNameToHex[trimmed]; - } + // Use canvas to parse any CSS color value + const ctx = document.createElement('canvas').getContext('2d'); + if (!ctx) return null; - // Otherwise treat as hex - return trimmed.startsWith('#') ? trimmed : `#${trimmed}`; + ctx.fillStyle = trimmed; + // The browser normalizes the color to a hex string (or rgb() for transparent colors) + return ctx.fillStyle; }; /** @@ -218,10 +68,10 @@ export const getLuminance = (color: string): number => { }; /** - * Determines if a color is considered "light" - * A color is considered light if its luminance is greater than 0.5 + * Determines if dark mode should be used based on the color + * Dark mode is enabled when the color is light (luminance > 0.5) * This threshold can be adjusted based on needs */ -export const isLightColor = (color: string, threshold = 0.5): boolean => { +export const darkMode = (color: string, threshold = 0.5): boolean => { return getLuminance(color) > threshold; }; From d5a58d9df2e759150af8a293f81f5725452b0e48 Mon Sep 17 00:00:00 2001 From: David Klakurka Date: Sat, 29 Nov 2025 11:02:52 -0800 Subject: [PATCH 3/4] Debugging --- react/lib/components/Widget/Widget.tsx | 6 +++++- react/lib/util/color.ts | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 7037b5a9..ae534c12 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -323,7 +323,11 @@ export const Widget: React.FunctionComponent = props => { const [isAboveMinimumAltpaymentAmount, setIsAboveMinimumAltpaymentAmount] = useState(null) const theme = useTheme(props.theme, isValidXecAddress(to)) - const isDarkMode = useMemo(() => darkMode(theme.palette.tertiary), [theme.palette.tertiary]) + const isDarkMode = useMemo(() => { + const result = darkMode(theme.palette.tertiary); + console.log('darkMode debug:', { tertiary: theme.palette.tertiary, isDarkMode: result }); + return result; + }, [theme.palette.tertiary]) const [thisAmount, setThisAmount] = useState(props.amount) const [thisCurrencyObject, setThisCurrencyObject] = useState(props.currencyObject) diff --git a/react/lib/util/color.ts b/react/lib/util/color.ts index 6a173cf0..a23eb9c0 100644 --- a/react/lib/util/color.ts +++ b/react/lib/util/color.ts @@ -73,5 +73,8 @@ export const getLuminance = (color: string): number => { * This threshold can be adjusted based on needs */ export const darkMode = (color: string, threshold = 0.5): boolean => { - return getLuminance(color) > threshold; + const luminance = getLuminance(color); + const result = luminance > threshold; + console.log('darkMode:', { color, luminance, threshold, result }); + return result; }; From a2685bb42b4be4004c2edb70ab89a484d9923fad Mon Sep 17 00:00:00 2001 From: David Klakurka Date: Sat, 29 Nov 2025 11:54:44 -0800 Subject: [PATCH 4/4] Removed debug code, fixed test --- react/lib/components/Widget/Widget.tsx | 6 +----- react/lib/tests/components/Widget.test.tsx | 2 +- react/lib/util/color.ts | 5 +---- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index ae534c12..7037b5a9 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -323,11 +323,7 @@ export const Widget: React.FunctionComponent = props => { const [isAboveMinimumAltpaymentAmount, setIsAboveMinimumAltpaymentAmount] = useState(null) const theme = useTheme(props.theme, isValidXecAddress(to)) - const isDarkMode = useMemo(() => { - const result = darkMode(theme.palette.tertiary); - console.log('darkMode debug:', { tertiary: theme.palette.tertiary, isDarkMode: result }); - return result; - }, [theme.palette.tertiary]) + const isDarkMode = useMemo(() => darkMode(theme.palette.tertiary), [theme.palette.tertiary]) const [thisAmount, setThisAmount] = useState(props.amount) const [thisCurrencyObject, setThisCurrencyObject] = useState(props.currencyObject) diff --git a/react/lib/tests/components/Widget.test.tsx b/react/lib/tests/components/Widget.test.tsx index 7c3da136..a9dcfd90 100644 --- a/react/lib/tests/components/Widget.test.tsx +++ b/react/lib/tests/components/Widget.test.tsx @@ -6,7 +6,7 @@ import Widget from '../../components/Widget/Widget' jest.mock('copy-to-clipboard', () => jest.fn()) jest.mock('../../util', () => ({ - __esModule: true, + ...jest.requireActual('../../util'), // network / balance getAddressBalance: jest.fn().mockResolvedValue(0), // address / currency helpers diff --git a/react/lib/util/color.ts b/react/lib/util/color.ts index a23eb9c0..6a173cf0 100644 --- a/react/lib/util/color.ts +++ b/react/lib/util/color.ts @@ -73,8 +73,5 @@ export const getLuminance = (color: string): number => { * This threshold can be adjusted based on needs */ export const darkMode = (color: string, threshold = 0.5): boolean => { - const luminance = getLuminance(color); - const result = luminance > threshold; - console.log('darkMode:', { color, luminance, threshold, result }); - return result; + return getLuminance(color) > threshold; };