@umituz/web-design-system 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +235 -0
  3. package/package.json +64 -0
  4. package/src/domain/tokens/color.tokens.ts +95 -0
  5. package/src/domain/tokens/index.ts +44 -0
  6. package/src/domain/tokens/radius.tokens.ts +27 -0
  7. package/src/domain/tokens/shadow.tokens.ts +25 -0
  8. package/src/domain/tokens/spacing.tokens.ts +65 -0
  9. package/src/domain/tokens/typography.tokens.ts +74 -0
  10. package/src/domain/types/component.types.ts +23 -0
  11. package/src/domain/types/index.ts +14 -0
  12. package/src/index.ts +28 -0
  13. package/src/infrastructure/constants/component.constants.ts +24 -0
  14. package/src/infrastructure/constants/index.ts +13 -0
  15. package/src/infrastructure/utils/cn.util.ts +13 -0
  16. package/src/infrastructure/utils/index.ts +10 -0
  17. package/src/presentation/atoms/Badge.tsx +46 -0
  18. package/src/presentation/atoms/Button.tsx +54 -0
  19. package/src/presentation/atoms/Icon.tsx +37 -0
  20. package/src/presentation/atoms/Input.tsx +44 -0
  21. package/src/presentation/atoms/Spinner.tsx +39 -0
  22. package/src/presentation/atoms/Text.tsx +64 -0
  23. package/src/presentation/atoms/index.ts +23 -0
  24. package/src/presentation/hooks/index.ts +13 -0
  25. package/src/presentation/hooks/useLocalStorage.ts +44 -0
  26. package/src/presentation/hooks/useMediaQuery.ts +46 -0
  27. package/src/presentation/hooks/useTheme.ts +51 -0
  28. package/src/presentation/molecules/Avatar.tsx +52 -0
  29. package/src/presentation/molecules/Chip.tsx +58 -0
  30. package/src/presentation/molecules/FormField.tsx +61 -0
  31. package/src/presentation/molecules/SearchBox.tsx +45 -0
  32. package/src/presentation/molecules/Toggle.tsx +61 -0
  33. package/src/presentation/molecules/index.ts +20 -0
  34. package/src/presentation/organisms/Alert.tsx +96 -0
  35. package/src/presentation/organisms/Card.tsx +90 -0
  36. package/src/presentation/organisms/Modal.tsx +130 -0
  37. package/src/presentation/organisms/Navbar.tsx +74 -0
  38. package/src/presentation/organisms/index.ts +17 -0
  39. package/src/presentation/templates/Form.tsx +41 -0
  40. package/src/presentation/templates/List.tsx +46 -0
  41. package/src/presentation/templates/Section.tsx +39 -0
  42. package/src/presentation/templates/index.ts +14 -0
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @umituz/web-design-system
3
+ *
4
+ * Web Design System - Atomic Design components (Atoms, Molecules, Organisms)
5
+ * for React applications with Tailwind CSS
6
+ *
7
+ * ⚠️ ONEMLI: App'ler bu root barrel'i kullanMAMALI.
8
+ * Subpath import kullanin: "@umituz/web-design-system/atoms"
9
+ *
10
+ * @example
11
+ * // ✅ DOGRU: Subpath import
12
+ * import { Button, Input } from '@umituz/web-design-system/atoms';
13
+ * import { Card } from '@umituz/web-design-system/organisms';
14
+ *
15
+ * // ❌ YANLIS: Root barrel import
16
+ * import { Button, Card } from '@umituz/web-design-system';
17
+ */
18
+
19
+ // Re-export everything for backward compatibility
20
+ export * from './domain/tokens';
21
+ export * from './domain/types';
22
+ export * from './infrastructure/utils';
23
+ export * from './infrastructure/constants';
24
+ export * from './presentation/atoms';
25
+ export * from './presentation/molecules';
26
+ export * from './presentation/organisms';
27
+ export * from './presentation/templates';
28
+ export * from './presentation/hooks';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Component Constants
3
+ * @description Default component configurations
4
+ */
5
+
6
+ export const DEFAULT_SIZE = 'md' as const;
7
+ export const DEFAULT_VARIANT = 'primary' as const;
8
+ export const DEFAULT_COLOR_SCHEME = 'light' as const;
9
+
10
+ export const SIZE_MAP = {
11
+ xs: 'xs',
12
+ sm: 'sm',
13
+ md: 'md',
14
+ lg: 'lg',
15
+ xl: 'xl',
16
+ } as const;
17
+
18
+ export const COLOR_MAP = {
19
+ primary: 'primary',
20
+ secondary: 'secondary',
21
+ success: 'success',
22
+ warning: 'warning',
23
+ destructive: 'destructive',
24
+ } as const;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Infrastructure Constants
3
+ * @description Constant exports
4
+ * Subpath: @umituz/web-design-system/constants
5
+ */
6
+
7
+ export {
8
+ DEFAULT_SIZE,
9
+ DEFAULT_VARIANT,
10
+ DEFAULT_COLOR_SCHEME,
11
+ SIZE_MAP,
12
+ COLOR_MAP,
13
+ } from './component.constants';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * cn Utility
3
+ * @description Conditional className utility (clsx + tailwind-merge alternative)
4
+ */
5
+
6
+ export type ClassName = string | undefined | null | false | ClassName[];
7
+
8
+ export function cn(...classes: ClassName[]): string {
9
+ return classes
10
+ .flat(Infinity as any)
11
+ .filter(Boolean)
12
+ .join(' ');
13
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Infrastructure Utils
3
+ * @description Utility function exports
4
+ * Subpath: @umituz/web-design-system/utils
5
+ */
6
+
7
+ export {
8
+ cn,
9
+ type ClassName,
10
+ } from './cn.util';
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Badge Component (Atom)
3
+ * @description Small status or label indicator
4
+ */
5
+
6
+ import { forwardRef, type HTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps, ColorVariant, SizeVariant } from '../../domain/types';
9
+
10
+ export interface BadgeProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
11
+ variant?: ColorVariant;
12
+ size?: Extract<SizeVariant, 'sm' | 'md' | 'lg'>;
13
+ }
14
+
15
+ const variantStyles: Record<ColorVariant, string> = {
16
+ primary: 'bg-primary text-primary-foreground border-primary',
17
+ secondary: 'bg-secondary text-secondary-foreground border-secondary',
18
+ success: 'bg-success text-success-foreground border-success',
19
+ warning: 'bg-warning text-warning-foreground border-warning',
20
+ destructive: 'bg-destructive text-destructive-foreground border-destructive',
21
+ };
22
+
23
+ const sizeStyles: Record<'sm' | 'md' | 'lg', string> = {
24
+ sm: 'px-2 py-0.5 text-xs',
25
+ md: 'px-2.5 py-1 text-sm',
26
+ lg: 'px-3 py-1.5 text-base',
27
+ };
28
+
29
+ export const Badge = forwardRef<HTMLDivElement, BadgeProps>(
30
+ ({ className, variant = 'primary', size = 'md', ...props }, ref) => {
31
+ return (
32
+ <div
33
+ ref={ref}
34
+ className={cn(
35
+ 'inline-flex items-center rounded-full border font-medium',
36
+ variantStyles[variant],
37
+ sizeStyles[size],
38
+ className
39
+ )}
40
+ {...props}
41
+ />
42
+ );
43
+ }
44
+ );
45
+
46
+ Badge.displayName = 'Badge';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Button Component (Atom)
3
+ * @description Interactive button element
4
+ */
5
+
6
+ import { forwardRef, type ButtonHTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps, SizeVariant, ColorVariant } from '../../domain/types';
9
+
10
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, BaseProps {
11
+ variant?: ColorVariant;
12
+ size?: SizeVariant;
13
+ fullWidth?: boolean;
14
+ }
15
+
16
+ const variantStyles: Record<ColorVariant, string> = {
17
+ primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
18
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19
+ success: 'bg-success text-success-foreground hover:bg-success/90',
20
+ warning: 'bg-warning text-warning-foreground hover:bg-warning/90',
21
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
22
+ };
23
+
24
+ const sizeStyles: Record<SizeVariant, string> = {
25
+ xs: 'h-7 px-2 text-xs',
26
+ sm: 'h-8 px-3 text-sm',
27
+ md: 'h-9 px-4 text-sm',
28
+ lg: 'h-10 px-5 text-base',
29
+ xl: 'h-11 px-6 text-lg',
30
+ };
31
+
32
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
33
+ ({ className, variant = 'primary', size = 'md', fullWidth = false, disabled, type = 'button', ...props }, ref) => {
34
+ return (
35
+ <button
36
+ ref={ref}
37
+ type={type}
38
+ disabled={disabled}
39
+ className={cn(
40
+ 'inline-flex items-center justify-center rounded-md font-medium transition-colors',
41
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
42
+ 'disabled:pointer-events-none disabled:opacity-50',
43
+ variantStyles[variant],
44
+ sizeStyles[size],
45
+ fullWidth && 'w-full',
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+ );
53
+
54
+ Button.displayName = 'Button';
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Icon Component (Atom)
3
+ * @description Wrapper for SVG icons
4
+ */
5
+
6
+ import { forwardRef, type SVGAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps, SizeVariant } from '../../domain/types';
9
+
10
+ export interface IconProps extends SVGAttributes<SVGSVGElement>, BaseProps {
11
+ size?: Extract<SizeVariant, 'xs' | 'sm' | 'md' | 'lg' | 'xl'>;
12
+ }
13
+
14
+ const sizeStyles: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl', string> = {
15
+ xs: 'h-3 w-3',
16
+ sm: 'h-4 w-4',
17
+ md: 'h-5 w-5',
18
+ lg: 'h-6 w-6',
19
+ xl: 'h-8 w-8',
20
+ };
21
+
22
+ export const Icon = forwardRef<SVGSVGElement, IconProps>(
23
+ ({ className, size = 'md', viewBox = '0 0 24 24', fill = 'none', xmlns = 'http://www.w3.org/2000/svg', ...props }, ref) => {
24
+ return (
25
+ <svg
26
+ ref={ref}
27
+ viewBox={viewBox}
28
+ fill={fill}
29
+ xmlns={xmlns}
30
+ className={cn('inline-block shrink-0', sizeStyles[size], className)}
31
+ {...props}
32
+ />
33
+ );
34
+ }
35
+ );
36
+
37
+ Icon.displayName = 'Icon';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Input Component (Atom)
3
+ * @description Text input field
4
+ */
5
+
6
+ import { forwardRef, type InputHTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps, SizeVariant } from '../../domain/types';
9
+
10
+ export interface InputProps extends InputHTMLAttributes<HTMLInputElement>, BaseProps {
11
+ error?: boolean;
12
+ size?: Extract<SizeVariant, 'sm' | 'md' | 'lg'>;
13
+ }
14
+
15
+ const sizeStyles: Record<'sm' | 'md' | 'lg', string> = {
16
+ sm: 'h-8 px-3 text-sm',
17
+ md: 'h-9 px-3 text-sm',
18
+ lg: 'h-10 px-4 text-base',
19
+ };
20
+
21
+ export const Input = forwardRef<HTMLInputElement, InputProps>(
22
+ ({ className, error, size = 'md', type = 'text', ...props }, ref) => {
23
+ return (
24
+ <input
25
+ ref={ref}
26
+ type={type}
27
+ className={cn(
28
+ 'flex w-full rounded-md border bg-background px-3 py-2',
29
+ 'text-sm ring-offset-background',
30
+ 'file:border-0 file:bg-transparent file:text-sm file:font-medium',
31
+ 'placeholder:text-muted-foreground',
32
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
33
+ 'disabled:cursor-not-allowed disabled:opacity-50',
34
+ sizeStyles[size],
35
+ error ? 'border-destructive' : 'border-input',
36
+ className
37
+ )}
38
+ {...props}
39
+ />
40
+ );
41
+ }
42
+ );
43
+
44
+ Input.displayName = 'Input';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Spinner Component (Atom)
3
+ * @description Loading indicator
4
+ */
5
+
6
+ import { forwardRef, type HTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps, SizeVariant } from '../../domain/types';
9
+
10
+ export interface SpinnerProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
11
+ size?: Extract<SizeVariant, 'sm' | 'md' | 'lg' | 'xl'>;
12
+ }
13
+
14
+ const sizeStyles: Record<'sm' | 'md' | 'lg' | 'xl', string> = {
15
+ sm: 'h-4 w-4 border-2',
16
+ md: 'h-6 w-6 border-2',
17
+ lg: 'h-8 w-8 border-3',
18
+ xl: 'h-12 w-12 border-4',
19
+ };
20
+
21
+ export const Spinner = forwardRef<HTMLDivElement, SpinnerProps>(
22
+ ({ className, size = 'md', ...props }, ref) => {
23
+ return (
24
+ <div
25
+ ref={ref}
26
+ className={cn(
27
+ 'animate-spin rounded-full border-primary border-t-transparent',
28
+ sizeStyles[size],
29
+ className
30
+ )}
31
+ role="status"
32
+ aria-label="Loading"
33
+ {...props}
34
+ />
35
+ );
36
+ }
37
+ );
38
+
39
+ Spinner.displayName = 'Spinner';
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Text Component (Atom)
3
+ * @description Styled text element
4
+ */
5
+
6
+ import { forwardRef, type HTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps } from '../../domain/types';
9
+
10
+ export type TextElement = 'p' | 'span' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
11
+ export type TextVariant = 'body' | 'heading' | 'label' | 'caption';
12
+ export type TextSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
13
+
14
+ export interface TextProps extends HTMLAttributes<HTMLElement>, BaseProps {
15
+ as?: TextElement;
16
+ variant?: TextVariant;
17
+ size?: TextSize;
18
+ weight?: 'normal' | 'medium' | 'semibold' | 'bold';
19
+ }
20
+
21
+ const variantStyles: Record<TextVariant, string> = {
22
+ body: 'text-foreground',
23
+ heading: 'text-foreground font-semibold',
24
+ label: 'text-foreground font-medium',
25
+ caption: 'text-muted-foreground',
26
+ };
27
+
28
+ const sizeStyles: Record<TextSize, string> = {
29
+ xs: 'text-xs',
30
+ sm: 'text-sm',
31
+ md: 'text-base',
32
+ lg: 'text-lg',
33
+ xl: 'text-xl',
34
+ '2xl': 'text-2xl',
35
+ '3xl': 'text-3xl',
36
+ '4xl': 'text-4xl',
37
+ };
38
+
39
+ const weightStyles: Record<'normal' | 'medium' | 'semibold' | 'bold', string> = {
40
+ normal: 'font-normal',
41
+ medium: 'font-medium',
42
+ semibold: 'font-semibold',
43
+ bold: 'font-bold',
44
+ };
45
+
46
+ export const Text = forwardRef<HTMLElement, TextProps>(
47
+ ({ className, as = 'p', variant = 'body', size = 'md', weight = 'normal', ...props }, ref) => {
48
+ const Tag = as as any;
49
+ return (
50
+ <Tag
51
+ ref={ref}
52
+ className={cn(
53
+ variantStyles[variant],
54
+ sizeStyles[size],
55
+ weightStyles[weight],
56
+ className
57
+ )}
58
+ {...props}
59
+ />
60
+ );
61
+ }
62
+ );
63
+
64
+ Text.displayName = 'Text';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Atoms Export
3
+ * @description Atomic components - smallest UI elements
4
+ * Subpath: @umituz/web-design-system/atoms
5
+ */
6
+
7
+ export { Button } from './Button';
8
+ export type { ButtonProps } from './Button';
9
+
10
+ export { Badge } from './Badge';
11
+ export type { BadgeProps } from './Badge';
12
+
13
+ export { Input } from './Input';
14
+ export type { InputProps } from './Input';
15
+
16
+ export { Text } from './Text';
17
+ export type { TextProps, TextElement, TextVariant, TextSize } from './Text';
18
+
19
+ export { Icon } from './Icon';
20
+ export type { IconProps } from './Icon';
21
+
22
+ export { Spinner } from './Spinner';
23
+ export type { SpinnerProps } from './Spinner';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Presentation Hooks Export
3
+ * @description React hooks
4
+ * Subpath: @umituz/web-design-system/hooks
5
+ */
6
+
7
+ export { useTheme } from './useTheme';
8
+ export type { Theme, UseThemeReturn } from './useTheme';
9
+
10
+ export { useMediaQuery, useBreakpoint } from './useMediaQuery';
11
+ export type { Breakpoint } from './useMediaQuery';
12
+
13
+ export { useLocalStorage } from './useLocalStorage';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * useLocalStorage Hook
3
+ * @description LocalStorage state management
4
+ */
5
+
6
+ import { useCallback, useEffect, useState } from 'react';
7
+
8
+ export function useLocalStorage<T>(
9
+ key: string,
10
+ initialValue: T
11
+ ): [T, (value: T | ((prev: T) => T)) => void, () => void] {
12
+ const [storedValue, setStoredValue] = useState<T>(() => {
13
+ try {
14
+ const item = window.localStorage.getItem(key);
15
+ return item ? JSON.parse(item) : initialValue;
16
+ } catch {
17
+ return initialValue;
18
+ }
19
+ });
20
+
21
+ const setValue = useCallback(
22
+ (value: T | ((prev: T) => T)) => {
23
+ try {
24
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
25
+ setStoredValue(valueToStore);
26
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
27
+ } catch (error) {
28
+ console.error(`Error setting localStorage key "${key}":`, error);
29
+ }
30
+ },
31
+ [key, storedValue]
32
+ );
33
+
34
+ const removeValue = useCallback(() => {
35
+ try {
36
+ window.localStorage.removeItem(key);
37
+ setStoredValue(initialValue);
38
+ } catch (error) {
39
+ console.error(`Error removing localStorage key "${key}":`, error);
40
+ }
41
+ }, [key, initialValue]);
42
+
43
+ return [storedValue, setValue, removeValue];
44
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * useMediaQuery Hook
3
+ * @description Responsive breakpoint detection
4
+ */
5
+
6
+ import { useEffect, useState } from 'react';
7
+
8
+ export type Breakpoint = 'sm' | 'md' | 'lg' | 'xl' | '2xl';
9
+
10
+ const breakpointValues: Record<Breakpoint, number> = {
11
+ sm: 640,
12
+ md: 768,
13
+ lg: 1024,
14
+ xl: 1280,
15
+ '2xl': 1536,
16
+ };
17
+
18
+ export function useMediaQuery(breakpoint: Breakpoint): boolean {
19
+ const [matches, setMatches] = useState(false);
20
+
21
+ useEffect(() => {
22
+ const media = window.matchMedia(`(min-width: ${breakpointValues[breakpoint]}px)`);
23
+ setMatches(media.matches);
24
+
25
+ const listener = () => setMatches(media.matches);
26
+ media.addEventListener('change', listener);
27
+ return () => media.removeEventListener('change', listener);
28
+ }, [breakpoint]);
29
+
30
+ return matches;
31
+ }
32
+
33
+ export function useBreakpoint(): Breakpoint | null {
34
+ const isSm = useMediaQuery('sm');
35
+ const isMd = useMediaQuery('md');
36
+ const isLg = useMediaQuery('lg');
37
+ const isXl = useMediaQuery('xl');
38
+ const is2xl = useMediaQuery('2xl');
39
+
40
+ if (is2xl) return '2xl';
41
+ if (isXl) return 'xl';
42
+ if (isLg) return 'lg';
43
+ if (isMd) return 'md';
44
+ if (isSm) return 'sm';
45
+ return null;
46
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * useTheme Hook
3
+ * @description Theme toggle functionality
4
+ */
5
+
6
+ import { useEffect, useState } from 'react';
7
+
8
+ export type Theme = 'light' | 'dark';
9
+
10
+ export interface UseThemeReturn {
11
+ theme: Theme;
12
+ toggleTheme: () => void;
13
+ setTheme: (theme: Theme) => void;
14
+ }
15
+
16
+ export function useTheme(): UseThemeReturn {
17
+ const [theme, setThemeState] = useState<Theme>(() => {
18
+ try {
19
+ const saved = localStorage.getItem('theme') as Theme | null;
20
+ if (saved === 'light' || saved === 'dark') return saved;
21
+ return 'light';
22
+ } catch {
23
+ return 'light';
24
+ }
25
+ });
26
+
27
+ useEffect(() => {
28
+ const root = document.documentElement;
29
+ root.classList.remove('light', 'dark');
30
+ root.classList.add(theme);
31
+ try {
32
+ localStorage.setItem('theme', theme);
33
+ } catch {
34
+ // Ignore
35
+ }
36
+ }, [theme]);
37
+
38
+ const toggleTheme = () => {
39
+ setThemeState((prev) => (prev === 'light' ? 'dark' : 'light'));
40
+ };
41
+
42
+ const setTheme = (newTheme: Theme) => {
43
+ setThemeState(newTheme);
44
+ };
45
+
46
+ return {
47
+ theme,
48
+ toggleTheme,
49
+ setTheme,
50
+ };
51
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Avatar Component (Molecule)
3
+ * @description User avatar with fallback
4
+ */
5
+
6
+ import { forwardRef, type HTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps, SizeVariant } from '../../domain/types';
9
+
10
+ export interface AvatarProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
11
+ src?: string;
12
+ alt?: string;
13
+ fallback?: string;
14
+ size?: Extract<SizeVariant, 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'>;
15
+ }
16
+
17
+ const sizeStyles: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl', string> = {
18
+ xs: 'h-6 w-6 text-xs',
19
+ sm: 'h-8 w-8 text-sm',
20
+ md: 'h-10 w-10 text-base',
21
+ lg: 'h-12 w-12 text-lg',
22
+ xl: 'h-16 w-16 text-xl',
23
+ '2xl': 'h-20 w-20 text-2xl',
24
+ };
25
+
26
+ export const Avatar = forwardRef<HTMLDivElement, AvatarProps>(
27
+ ({ className, src, alt, fallback, size = 'md', ...props }, ref) => {
28
+ const hasError = !src;
29
+
30
+ return (
31
+ <div
32
+ ref={ref}
33
+ className={cn(
34
+ 'relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted',
35
+ sizeStyles[size],
36
+ className
37
+ )}
38
+ {...props}
39
+ >
40
+ {hasError ? (
41
+ <span className="font-medium text-muted-foreground">
42
+ {fallback || '?'}
43
+ </span>
44
+ ) : (
45
+ <img src={src} alt={alt || 'Avatar'} className="h-full w-full object-cover" />
46
+ )}
47
+ </div>
48
+ );
49
+ }
50
+ );
51
+
52
+ Avatar.displayName = 'Avatar';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Chip Component (Molecule)
3
+ * @description Selectable/removable tag
4
+ */
5
+
6
+ import { forwardRef, type ReactNode } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps, ColorVariant } from '../../domain/types';
9
+ import { Badge } from '../atoms/Badge';
10
+ import { Icon } from '../atoms/Icon';
11
+
12
+ export interface ChipProps extends BaseProps {
13
+ label: string;
14
+ variant?: ColorVariant;
15
+ onRemove?: () => void;
16
+ onClick?: () => void;
17
+ removable?: boolean;
18
+ }
19
+
20
+ export const Chip = forwardRef<HTMLDivElement, ChipProps>(
21
+ ({ label, variant = 'secondary', onRemove, onClick, removable = true, className, ...props }, ref) => {
22
+ return (
23
+ <Badge
24
+ ref={ref}
25
+ variant={variant}
26
+ className={cn(
27
+ 'gap-1.5 pr-2',
28
+ onClick && 'cursor-pointer hover:opacity-80',
29
+ className
30
+ )}
31
+ onClick={onClick}
32
+ {...props}
33
+ >
34
+ <span>{label}</span>
35
+ {removable && onRemove && (
36
+ <button
37
+ type="button"
38
+ onClick={(e) => {
39
+ e.stopPropagation();
40
+ onRemove();
41
+ }}
42
+ className="hover:opacity-70"
43
+ >
44
+ <Icon size="xs">
45
+ <path
46
+ strokeLinecap="round"
47
+ strokeLinejoin="round"
48
+ d="M6 18L18 6M6 6l12 12"
49
+ />
50
+ </Icon>
51
+ </button>
52
+ )}
53
+ </Badge>
54
+ );
55
+ }
56
+ );
57
+
58
+ Chip.displayName = 'Chip';