feat(ui): add Button, Modal, Spinner, Toast, and Tooltip components with styles
All checks were successful
Build and Release / build-and-release (push) Successful in 13m12s
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.
This commit is contained in:
93
packages/ui/src/Avatar.tsx
Normal file
93
packages/ui/src/Avatar.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user