All checks were successful
Build and Release / build-and-release (push) Successful in 13m12s
- Implemented Button component with various props for customization. - Created Modal component with header, content, and footer subcomponents. - Added Spinner component for loading indicators. - Developed Toast component for displaying notifications. - Introduced Tooltip component for contextual hints with keyboard shortcuts. - Added corresponding CSS modules for styling each component. - Updated index file to export new components. - Configured TypeScript settings for the UI package.
94 lines
2.5 KiB
TypeScript
94 lines
2.5 KiB
TypeScript
import clsx from 'clsx';
|
|
import type { CSSProperties } from 'react';
|
|
import styles from './Avatar.module.css';
|
|
|
|
export interface AvatarProps {
|
|
src?: string | null;
|
|
alt?: string;
|
|
size?: number;
|
|
fallback?: string;
|
|
status?: 'online' | 'idle' | 'dnd' | 'offline';
|
|
className?: string;
|
|
style?: CSSProperties;
|
|
onClick?: () => void;
|
|
}
|
|
|
|
function getInitials(name: string): string {
|
|
return name
|
|
.split(/\s+/)
|
|
.map((w) => w[0])
|
|
.join('')
|
|
.toUpperCase()
|
|
.slice(0, 2);
|
|
}
|
|
|
|
function stringToColor(str: string): string {
|
|
let hash = 0;
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
}
|
|
const hue = Math.abs(hash % 360);
|
|
return `hsl(${hue}, 60%, 50%)`;
|
|
}
|
|
|
|
function getStatusGeometry(avatarSize: number): { dotSize: number; borderWidth: number; offset: number } {
|
|
if (avatarSize <= 16) return { dotSize: 8, borderWidth: 0, offset: -1 };
|
|
if (avatarSize <= 20) return { dotSize: 8, borderWidth: 0, offset: -1 };
|
|
if (avatarSize <= 24) return { dotSize: 10, borderWidth: 2, offset: -2 };
|
|
if (avatarSize <= 32) return { dotSize: 10, borderWidth: 3, offset: -3 };
|
|
if (avatarSize <= 36) return { dotSize: 10, borderWidth: 3, offset: -3 };
|
|
if (avatarSize <= 40) return { dotSize: 12, borderWidth: 3, offset: -3 };
|
|
if (avatarSize <= 48) return { dotSize: 14, borderWidth: 3, offset: -3 };
|
|
if (avatarSize <= 56) return { dotSize: 16, borderWidth: 3, offset: -3 };
|
|
if (avatarSize <= 80) return { dotSize: 16, borderWidth: 6, offset: -4 };
|
|
return { dotSize: 24, borderWidth: 8, offset: -6 };
|
|
}
|
|
|
|
export function Avatar({ src, alt = '', size = 40, fallback, status, className, style, onClick }: AvatarProps) {
|
|
const initials = fallback ? getInitials(fallback) : alt ? getInitials(alt) : '?';
|
|
const bgColor = stringToColor(fallback || alt || '?');
|
|
const geo = getStatusGeometry(size);
|
|
|
|
return (
|
|
<div
|
|
className={clsx(styles.wrapper, className)}
|
|
style={{
|
|
width: size,
|
|
height: size,
|
|
cursor: onClick ? 'pointer' : undefined,
|
|
...style,
|
|
}}
|
|
onClick={onClick}
|
|
>
|
|
{src ? (
|
|
<img src={src} alt={alt} className={styles.image} />
|
|
) : (
|
|
<div
|
|
className={styles.fallback}
|
|
style={{
|
|
width: size,
|
|
height: size,
|
|
backgroundColor: bgColor,
|
|
fontSize: size * 0.4,
|
|
userSelect: 'none',
|
|
}}
|
|
>
|
|
{initials}
|
|
</div>
|
|
)}
|
|
{status && (
|
|
<div
|
|
className={clsx(styles.statusBadge, styles[status])}
|
|
style={{
|
|
width: geo.dotSize,
|
|
height: geo.dotSize,
|
|
borderWidth: geo.borderWidth,
|
|
bottom: geo.offset,
|
|
right: geo.offset,
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|