diff --git a/apps/client/src/components/common/search-input.tsx b/apps/client/src/components/common/search-input.tsx index 08cbbee0..27e50fd4 100644 --- a/apps/client/src/components/common/search-input.tsx +++ b/apps/client/src/components/common/search-input.tsx @@ -6,12 +6,14 @@ import { useTranslation } from "react-i18next"; export interface SearchInputProps { placeholder?: string; + ariaLabel?: string; debounceDelay?: number; onSearch: (value: string) => void; } export function SearchInput({ placeholder, + ariaLabel, debounceDelay = 500, onSearch, }: SearchInputProps) { @@ -28,6 +30,7 @@ export function SearchInput({ } value={value} onChange={(e) => setValue(e.currentTarget.value)} diff --git a/apps/client/src/components/ui/custom-avatar.tsx b/apps/client/src/components/ui/custom-avatar.tsx index 1342cdfb..5f38e804 100644 --- a/apps/client/src/components/ui/custom-avatar.tsx +++ b/apps/client/src/components/ui/custom-avatar.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Avatar } from "@mantine/core"; +import { Avatar, MantineColor } from "@mantine/core"; import { getAvatarUrl } from "@/lib/config.ts"; import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts"; @@ -16,11 +16,39 @@ interface CustomAvatarProps { mt?: string | number; } +// `color.shade` pairs whose filled background meets WCAG AA (4.5:1) against +// white text. Avoids lime/yellow/green/orange — even their dark shades have +// weak white-text contrast. +const SAFE_INITIALS_COLORS: MantineColor[] = [ + "blue.8", + "cyan.9", + "grape.7", + "indigo.7", + "pink.8", + "red.8", + "violet.7", +]; + +function hashName(input: string) { + let hash = 0; + for (let i = 0; i < input.length; i += 1) { + hash = (hash << 5) - hash + input.charCodeAt(i); + hash |= 0; + } + return Math.abs(hash); +} + +function pickInitialsColor(name: string) { + return SAFE_INITIALS_COLORS[hashName(name) % SAFE_INITIALS_COLORS.length]; +} + export const CustomAvatar = React.forwardRef< HTMLInputElement, CustomAvatarProps ->(({ avatarUrl, name, type, ...props }: CustomAvatarProps, ref) => { +>(({ avatarUrl, name, type, color, ...props }: CustomAvatarProps, ref) => { const avatarLink = getAvatarUrl(avatarUrl, type); + const resolvedColor = + !color || color === "initials" ? pickInitialsColor(name ?? "") : color; return ( );