mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: draw avatar initials in black or white so they read on every colour
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { Avatar } from '../avatar';
|
||||
import { getLuminance, parseColor } from '@/lib/color-transform';
|
||||
|
||||
// LV-10: white initials failed 4.5:1 on the light generated colours.
|
||||
|
||||
function ratio(fg: string, bg: string): number {
|
||||
const [la, lb] = [parseColor(fg)!, parseColor(bg)!].map((c) => getLuminance(c.r, c.g, c.b));
|
||||
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
||||
}
|
||||
|
||||
describe('avatar initials', () => {
|
||||
it('read at 4.5:1 on any generated colour', () => {
|
||||
for (const name of ['Alice Smith', 'Bob Chen', 'Carol Lee', 'Dev User', 'Erik Ström', 'Fatima Noor', 'Gao Wei', 'Hana Kim']) {
|
||||
const { container, unmount } = render(<Avatar name={name} disableImages />);
|
||||
const el = container.firstElementChild as HTMLElement;
|
||||
expect(ratio(el.style.color, el.style.backgroundColor)).toBeGreaterThanOrEqual(4.5);
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useCallback, useMemo, useEffect } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getLuminance, parseColor } from "@/lib/color-transform";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
import { useContactStore, getContactPhotoIndex, type ContactPhotoRef } from "@/stores/contact-store";
|
||||
import { useConfig } from "@/hooks/use-config";
|
||||
@@ -156,6 +157,18 @@ interface AvatarProps {
|
||||
fallbackColor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* White or black initials, whichever contrasts more with the background.
|
||||
* One of the two always reaches 4.58:1, so the initials pass 4.5:1 on every
|
||||
* generated colour (white alone failed on yellows and greens).
|
||||
*/
|
||||
function initialsColor(background: string): string {
|
||||
const rgb = parseColor(background);
|
||||
if (!rgb) return '#ffffff';
|
||||
const l = getLuminance(rgb.r, rgb.g, rgb.b);
|
||||
return 1.05 / (l + 0.05) >= (l + 0.05) / 0.05 ? '#ffffff' : '#000000';
|
||||
}
|
||||
|
||||
export function Avatar({ name, email, contactPhoto, size = "md", className, disableImages = false, disableFavicon = false, fallbackColor }: AvatarProps) {
|
||||
const [imgError, setImgError] = useState(false);
|
||||
const [pluginAvatarUrl, setPluginAvatarUrl] = useState<string | null>(null);
|
||||
@@ -272,6 +285,8 @@ export function Avatar({ name, email, contactPhoto, size = "md", className, disa
|
||||
}
|
||||
}, [imgSrc, pluginAvatar, faviconDomain, resolvedContactPhoto, customAvatar, profilePic]);
|
||||
|
||||
const initialsBackground = fallbackColor ?? getBackgroundColor();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={observe}
|
||||
@@ -280,7 +295,10 @@ export function Avatar({ name, email, contactPhoto, size = "md", className, disa
|
||||
sizeClasses[size],
|
||||
className
|
||||
)}
|
||||
style={{ backgroundColor: imgSrc ? (isFavicon ? "#ffffff" : "transparent") : (fallbackColor ?? getBackgroundColor()) }}
|
||||
style={{
|
||||
backgroundColor: imgSrc ? (isFavicon ? "#ffffff" : "transparent") : initialsBackground,
|
||||
color: imgSrc ? undefined : initialsColor(initialsBackground),
|
||||
}}
|
||||
title={name || email}
|
||||
>
|
||||
{imgSrc ? (
|
||||
|
||||
Reference in New Issue
Block a user