@umituz/web-design-system 5.3.1 → 5.4.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.
package/src/index.ts CHANGED
@@ -1,14 +1,20 @@
1
-
1
+ // Core Tokens, Types, Constants, Hooks & Utils
2
2
  export * from './domain/tokens';
3
3
  export * from './domain/types';
4
4
  export * from './infrastructure/utils';
5
5
  export * from './infrastructure/constants';
6
- export * from './presentation/templates';
7
6
  export * from './presentation/hooks';
8
7
 
8
+ // Templates
9
+ export * from './presentation/templates';
10
+
11
+ // Organisms
12
+ export * from './presentation/organisms';
13
+
14
+ // Atoms
9
15
  export type { ButtonProps } from './presentation/atoms/Button';
10
- export { Button } from './presentation/atoms/Button';
11
- export type { BadgeProps } from './presentation/atoms/Badge';
16
+ export { Button, buttonVariants } from './presentation/atoms/Button';
17
+ export type { BadgeProps, BadgeVariant } from './presentation/atoms/Badge';
12
18
  export { Badge } from './presentation/atoms/Badge';
13
19
  export type { InputProps } from './presentation/atoms/Input';
14
20
  export { Input } from './presentation/atoms/Input';
@@ -34,6 +40,8 @@ export type { TooltipProps } from './presentation/atoms/Tooltip';
34
40
  export { Tooltip } from './presentation/atoms/Tooltip';
35
41
  export type { ProgressProps } from './presentation/atoms/Progress';
36
42
  export { Progress } from './presentation/atoms/Progress';
43
+ export type { ProgressRingProps } from './presentation/atoms/ProgressRing';
44
+ export { ProgressRing } from './presentation/atoms/ProgressRing';
37
45
  export { Label } from './presentation/atoms/Label';
38
46
  export type { AspectRatioProps } from './presentation/atoms/AspectRatio';
39
47
  export { AspectRatio } from './presentation/atoms/AspectRatio';
@@ -41,18 +49,58 @@ export type { SwitchProps } from './presentation/atoms/Switch';
41
49
  export { Switch } from './presentation/atoms/Switch';
42
50
  export { Separator } from './presentation/atoms/Separator';
43
51
  export { Toggle as TogglePrimitive, toggleVariants } from './presentation/atoms/Toggle';
52
+ export { Show } from './presentation/atoms/Show';
53
+ export type { ShowComponentProps } from './presentation/atoms/Show';
54
+ export { Hide } from './presentation/atoms/Hide';
55
+ export type { HideComponentProps } from './presentation/atoms/Hide';
56
+ export { IconButton } from './presentation/atoms/IconButton';
57
+ export type { IconButtonProps } from './presentation/atoms/IconButton';
58
+ export { Sparkline } from './presentation/atoms/Sparkline';
59
+ export type { SparklineProps } from './presentation/atoms/Sparkline';
60
+ export { HeatmapStrip } from './presentation/atoms/HeatmapStrip';
61
+ export type { HeatmapStripProps, HeatmapCell } from './presentation/atoms/HeatmapStrip';
62
+ export { NativeSelect } from './presentation/atoms/NativeSelect';
63
+ export type { NativeSelectProps, NativeSelectOption } from './presentation/atoms/NativeSelect';
64
+ export { BatteryPill } from './presentation/atoms/BatteryPill';
65
+ export type { BatteryPillProps } from './presentation/atoms/BatteryPill';
66
+ export { CodeChip } from './presentation/atoms/CodeChip';
67
+ export type { CodeChipProps } from './presentation/atoms/CodeChip';
68
+ export { Kbd } from './presentation/atoms/Kbd';
69
+ export type { KbdProps } from './presentation/atoms/Kbd';
70
+ export { Money } from './presentation/atoms/Money';
71
+ export type { MoneyProps } from './presentation/atoms/Money';
72
+ export { CopyButton } from './presentation/atoms/CopyButton';
73
+ export type { CopyButtonProps } from './presentation/atoms/CopyButton';
74
+ export { StatusDot } from './presentation/atoms/StatusDot';
75
+ export type { StatusDotProps, StatusType } from './presentation/atoms/StatusDot';
44
76
 
77
+ // Molecules
45
78
  export type { FormFieldProps } from './presentation/molecules/FormField';
46
79
  export { FormField } from './presentation/molecules/FormField';
47
80
  export type { SearchBoxProps } from './presentation/molecules/SearchBox';
48
81
  export { SearchBox } from './presentation/molecules/SearchBox';
49
- export type { AvatarProps, AvatarImageProps, AvatarFallbackProps } from './presentation/molecules/Avatar';
82
+ export type {
83
+ AvatarProps,
84
+ AvatarImageProps,
85
+ AvatarFallbackProps,
86
+ } from './presentation/molecules/Avatar';
50
87
  export { Avatar, AvatarImage, AvatarFallback } from './presentation/molecules/Avatar';
51
88
  export type { ChipProps } from './presentation/molecules/Chip';
52
89
  export { Chip } from './presentation/molecules/Chip';
53
90
  export type { ToggleProps } from './presentation/molecules/Toggle';
54
91
  export { Toggle } from './presentation/molecules/Toggle';
55
- export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton } from './presentation/molecules/Select';
92
+ export {
93
+ Select,
94
+ SelectGroup,
95
+ SelectValue,
96
+ SelectTrigger,
97
+ SelectContent,
98
+ SelectLabel,
99
+ SelectItem,
100
+ SelectSeparator,
101
+ SelectScrollUpButton,
102
+ SelectScrollDownButton,
103
+ } from './presentation/molecules/Select';
56
104
  export type { TextareaProps } from './presentation/molecules/Textarea';
57
105
  export { Textarea } from './presentation/molecules/Textarea';
58
106
  export type { RadioGroupProps, RadioOption } from './presentation/molecules/RadioGroup';
@@ -62,9 +110,35 @@ export { CheckboxGroup } from './presentation/molecules/CheckboxGroup';
62
110
  export type { InputGroupProps } from './presentation/molecules/InputGroup';
63
111
  export { InputGroup, GroupedInput } from './presentation/molecules/InputGroup';
64
112
  export { ScrollArea, ScrollBar } from './presentation/molecules/ScrollArea';
65
- export type { ListItemProps } from './presentation/molecules/ListItem';
66
- export { ListItem } from './presentation/molecules/ListItem';
113
+ export type { ListItemProps as MoleculeListItemProps } from './presentation/molecules/ListItem';
114
+ export { ListItem as MoleculeListItem } from './presentation/molecules/ListItem';
67
115
  export type { BookmarkButtonProps, BookmarkTone } from './presentation/molecules/BookmarkButton';
68
116
  export { BookmarkButton } from './presentation/molecules/BookmarkButton';
69
-
70
- export * from './presentation/organisms';
117
+ export { PasswordInput } from './presentation/molecules/PasswordInput';
118
+ export type { PasswordInputProps } from './presentation/molecules/PasswordInput';
119
+ export { SearchInput } from './presentation/molecules/SearchInput';
120
+ export type { SearchInputProps } from './presentation/molecules/SearchInput';
121
+ export { ConfirmButton } from './presentation/molecules/ConfirmButton';
122
+ export type { ConfirmButtonProps } from './presentation/molecules/ConfirmButton';
123
+ export { Toolbar } from './presentation/molecules/Toolbar';
124
+ export type { ToolbarProps } from './presentation/molecules/Toolbar';
125
+ export { ActiveFilterTags } from './presentation/molecules/ActiveFilterTags';
126
+ export type { ActiveFilterTagsProps } from './presentation/molecules/ActiveFilterTags';
127
+ export { CodeBlock } from './presentation/molecules/CodeBlock';
128
+ export type { CodeBlockProps } from './presentation/molecules/CodeBlock';
129
+ export { OptionCard } from './presentation/molecules/OptionCard';
130
+ export type { OptionCardProps } from './presentation/molecules/OptionCard';
131
+ export { OptionTile } from './presentation/molecules/OptionTile';
132
+ export type { OptionTileProps, OptionTileState } from './presentation/molecules/OptionTile';
133
+ export { Field } from './presentation/molecules/Field';
134
+ export type { FieldProps } from './presentation/molecules/Field';
135
+ export { Segmented } from './presentation/molecules/Segmented';
136
+ export type { SegmentedProps, SegmentedOption } from './presentation/molecules/Segmented';
137
+ export { RatingStars } from './presentation/molecules/RatingStars';
138
+ export type { RatingStarsProps } from './presentation/molecules/RatingStars';
139
+ export { StatPill } from './presentation/molecules/StatPill';
140
+ export type { StatPillProps, StatPillTone } from './presentation/molecules/StatPill';
141
+ export { TagInput } from './presentation/molecules/TagInput';
142
+ export type { TagInputProps } from './presentation/molecules/TagInput';
143
+ export { MarkdownText } from './presentation/molecules/MarkdownText';
144
+ export type { MarkdownTextProps } from './presentation/molecules/MarkdownText';
@@ -0,0 +1,93 @@
1
+ import * as React from 'react';
2
+ import { Copy, Check } from 'lucide-react';
3
+ import { cn } from '../../infrastructure/utils';
4
+ import { useClipboard } from '../hooks/useClipboard';
5
+
6
+ export interface CopyButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
7
+ text: string;
8
+ label?: string;
9
+ copiedLabel?: string;
10
+ variant?: 'outline' | 'ghost' | 'secondary' | 'default';
11
+ size?: 'xs' | 'sm' | 'md' | 'lg';
12
+ showLabel?: boolean;
13
+ onCopySuccess?: (text: string) => void;
14
+ }
15
+
16
+ const sizeStyles = {
17
+ xs: 'h-6 px-2 text-xs gap-1 [&_svg]:size-3',
18
+ sm: 'h-8 px-2.5 text-xs gap-1.5 [&_svg]:size-3.5',
19
+ md: 'h-9 px-3 text-sm gap-2 [&_svg]:size-4',
20
+ lg: 'h-10 px-4 text-sm gap-2 [&_svg]:size-4',
21
+ };
22
+
23
+ const iconOnlySizeStyles = {
24
+ xs: 'h-6 w-6 p-0 [&_svg]:size-3',
25
+ sm: 'h-8 w-8 p-0 [&_svg]:size-3.5',
26
+ md: 'h-9 w-9 p-0 [&_svg]:size-4',
27
+ lg: 'h-10 w-10 p-0 [&_svg]:size-4',
28
+ };
29
+
30
+ const variantStyles = {
31
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
32
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
33
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
34
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
35
+ };
36
+
37
+ export const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
38
+ (
39
+ {
40
+ text,
41
+ label = 'Kopyala',
42
+ copiedLabel = 'Kopyalandı',
43
+ variant = 'outline',
44
+ size = 'sm',
45
+ showLabel = false,
46
+ className,
47
+ onClick,
48
+ onCopySuccess,
49
+ ...props
50
+ },
51
+ ref
52
+ ) => {
53
+ const { copied, copy } = useClipboard();
54
+
55
+ const handleCopy = async (e: React.MouseEvent<HTMLButtonElement>) => {
56
+ onClick?.(e);
57
+ if (e.defaultPrevented) return;
58
+ const success = await copy(text);
59
+ if (success) {
60
+ onCopySuccess?.(text);
61
+ }
62
+ };
63
+
64
+ return (
65
+ <button
66
+ ref={ref}
67
+ type="button"
68
+ aria-label={copied ? copiedLabel : label}
69
+ title={copied ? copiedLabel : label}
70
+ className={cn(
71
+ 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 shrink-0 select-none',
72
+ showLabel ? sizeStyles[size] : iconOnlySizeStyles[size],
73
+ variantStyles[variant],
74
+ copied && 'text-emerald-600 dark:text-emerald-400 border-emerald-500/40 bg-emerald-500/10 hover:bg-emerald-500/20',
75
+ className
76
+ )}
77
+ onClick={handleCopy}
78
+ {...props}
79
+ >
80
+ {copied ? (
81
+ <Check className="shrink-0 transition-transform duration-200 scale-110" />
82
+ ) : (
83
+ <Copy className="shrink-0" />
84
+ )}
85
+ {showLabel && (
86
+ <span>{copied ? copiedLabel : label}</span>
87
+ )}
88
+ </button>
89
+ );
90
+ }
91
+ );
92
+
93
+ CopyButton.displayName = 'CopyButton';
@@ -0,0 +1,79 @@
1
+ import * as React from 'react';
2
+ import { cn } from '../../infrastructure/utils';
3
+
4
+ export type StatusType =
5
+ | 'online'
6
+ | 'offline'
7
+ | 'busy'
8
+ | 'away'
9
+ | 'pending'
10
+ | 'success'
11
+ | 'warning'
12
+ | 'error';
13
+
14
+ export interface StatusDotProps extends React.HTMLAttributes<HTMLSpanElement> {
15
+ status: StatusType;
16
+ ping?: boolean;
17
+ label?: string;
18
+ size?: 'xs' | 'sm' | 'md' | 'lg';
19
+ }
20
+
21
+ const colorMap: Record<StatusType, { dot: string; ping: string }> = {
22
+ online: { dot: 'bg-emerald-500', ping: 'bg-emerald-400' },
23
+ success: { dot: 'bg-emerald-500', ping: 'bg-emerald-400' },
24
+ busy: { dot: 'bg-rose-500', ping: 'bg-rose-400' },
25
+ error: { dot: 'bg-rose-500', ping: 'bg-rose-400' },
26
+ away: { dot: 'bg-amber-500', ping: 'bg-amber-400' },
27
+ warning: { dot: 'bg-amber-500', ping: 'bg-amber-400' },
28
+ pending: { dot: 'bg-sky-500', ping: 'bg-sky-400' },
29
+ offline: { dot: 'bg-zinc-400 dark:bg-zinc-500', ping: 'bg-zinc-300' },
30
+ };
31
+
32
+ const dotSizeMap = {
33
+ xs: 'size-1.5',
34
+ sm: 'size-2',
35
+ md: 'size-2.5',
36
+ lg: 'size-3.5',
37
+ };
38
+
39
+ const textSizeMap = {
40
+ xs: 'text-xs',
41
+ sm: 'text-xs',
42
+ md: 'text-sm',
43
+ lg: 'text-base',
44
+ };
45
+
46
+ export const StatusDot = React.forwardRef<HTMLSpanElement, StatusDotProps>(
47
+ ({ status, ping = false, label, size = 'sm', className, ...props }, ref) => {
48
+ const colors = colorMap[status] ?? colorMap.offline;
49
+
50
+ return (
51
+ <span
52
+ ref={ref}
53
+ className={cn('inline-flex items-center gap-2 select-none', textSizeMap[size], className)}
54
+ {...props}
55
+ >
56
+ <span className="relative flex shrink-0 items-center justify-center">
57
+ {ping && status !== 'offline' && (
58
+ <span
59
+ className={cn(
60
+ 'absolute inline-flex h-full w-full animate-ping rounded-full opacity-75',
61
+ colors.ping
62
+ )}
63
+ />
64
+ )}
65
+ <span
66
+ className={cn(
67
+ 'relative inline-flex rounded-full',
68
+ dotSizeMap[size],
69
+ colors.dot
70
+ )}
71
+ />
72
+ </span>
73
+ {label && <span className="text-foreground/90 font-medium">{label}</span>}
74
+ </span>
75
+ );
76
+ }
77
+ );
78
+
79
+ StatusDot.displayName = 'StatusDot';
@@ -1,5 +1,4 @@
1
-
2
- export { Button } from './Button';
1
+ export { Button, buttonVariants } from './Button';
3
2
  export type { ButtonProps } from './Button';
4
3
 
5
4
  export { Badge } from './Badge';
@@ -60,7 +59,6 @@ export { Toggle, toggleVariants } from './Toggle';
60
59
 
61
60
  export { Show } from './Show';
62
61
  export type { ShowComponentProps } from './Show';
63
-
64
62
  export { Hide } from './Hide';
65
63
  export type { HideComponentProps } from './Hide';
66
64
 
@@ -68,9 +66,9 @@ export { IconButton } from './IconButton';
68
66
  export type { IconButtonProps } from './IconButton';
69
67
 
70
68
  export { Sparkline } from './Sparkline';
69
+ export type { SparklineProps } from './Sparkline';
71
70
  export { HeatmapStrip } from './HeatmapStrip';
72
71
  export type { HeatmapStripProps, HeatmapCell } from './HeatmapStrip';
73
- export type { SparklineProps } from './Sparkline';
74
72
 
75
73
  export { NativeSelect } from './NativeSelect';
76
74
  export type { NativeSelectProps, NativeSelectOption } from './NativeSelect';
@@ -86,3 +84,10 @@ export type { KbdProps } from './Kbd';
86
84
 
87
85
  export { Money } from './Money';
88
86
  export type { MoneyProps } from './Money';
87
+
88
+ // New Productivity Atoms
89
+ export { CopyButton } from './CopyButton';
90
+ export type { CopyButtonProps } from './CopyButton';
91
+
92
+ export { StatusDot } from './StatusDot';
93
+ export type { StatusDotProps, StatusType } from './StatusDot';
@@ -0,0 +1,125 @@
1
+ import * as React from 'react';
2
+ import { Button, type ButtonProps } from '../atoms/Button';
3
+ import { Check, X } from 'lucide-react';
4
+ import { cn } from '../../infrastructure/utils';
5
+
6
+ export interface ConfirmButtonProps extends Omit<ButtonProps, 'onConfirm'> {
7
+ onConfirm: () => void | Promise<void>;
8
+ confirmText?: string;
9
+ confirmLabel?: string;
10
+ cancelLabel?: string;
11
+ timeoutMs?: number;
12
+ }
13
+
14
+ export const ConfirmButton = React.forwardRef<HTMLButtonElement, ConfirmButtonProps>(
15
+ (
16
+ {
17
+ onConfirm,
18
+ confirmText = 'Emin misiniz?',
19
+ confirmLabel = 'Evet',
20
+ cancelLabel = 'İptal',
21
+ timeoutMs = 4000,
22
+ children,
23
+ variant = 'destructive',
24
+ size = 'default',
25
+ className,
26
+ disabled,
27
+ ...props
28
+ },
29
+ ref
30
+ ) => {
31
+ const [isConfirming, setIsConfirming] = React.useState(false);
32
+ const [isLoading, setIsLoading] = React.useState(false);
33
+ const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
34
+
35
+ React.useEffect(() => {
36
+ return () => {
37
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
38
+ };
39
+ }, []);
40
+
41
+ const resetConfirmation = () => {
42
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
43
+ setIsConfirming(false);
44
+ };
45
+
46
+ const handleInitialClick = (e: React.MouseEvent<HTMLButtonElement>) => {
47
+ e.preventDefault();
48
+ e.stopPropagation();
49
+ setIsConfirming(true);
50
+
51
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
52
+ timeoutRef.current = setTimeout(() => {
53
+ setIsConfirming(false);
54
+ }, timeoutMs);
55
+ };
56
+
57
+ const handleConfirm = async (e: React.MouseEvent) => {
58
+ e.preventDefault();
59
+ e.stopPropagation();
60
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
61
+
62
+ try {
63
+ setIsLoading(true);
64
+ await onConfirm();
65
+ } finally {
66
+ setIsLoading(false);
67
+ setIsConfirming(false);
68
+ }
69
+ };
70
+
71
+ const handleCancel = (e: React.MouseEvent) => {
72
+ e.preventDefault();
73
+ e.stopPropagation();
74
+ resetConfirmation();
75
+ };
76
+
77
+ if (isConfirming) {
78
+ return (
79
+ <div className={cn('inline-flex items-center gap-1.5 animate-in fade-in zoom-in-95 duration-150', className)}>
80
+ <span className="text-xs font-semibold text-destructive px-1 select-none">
81
+ {confirmText}
82
+ </span>
83
+ <Button
84
+ type="button"
85
+ size="xs"
86
+ variant="destructive"
87
+ loading={isLoading}
88
+ onClick={handleConfirm}
89
+ className="gap-1 h-7 px-2"
90
+ >
91
+ <Check className="size-3" />
92
+ <span>{confirmLabel}</span>
93
+ </Button>
94
+ <Button
95
+ type="button"
96
+ size="xs"
97
+ variant="ghost"
98
+ disabled={isLoading}
99
+ onClick={handleCancel}
100
+ className="h-7 px-1.5 text-muted-foreground hover:text-foreground"
101
+ >
102
+ <X className="size-3" />
103
+ <span className="sr-only">{cancelLabel}</span>
104
+ </Button>
105
+ </div>
106
+ );
107
+ }
108
+
109
+ return (
110
+ <Button
111
+ ref={ref}
112
+ variant={variant}
113
+ size={size}
114
+ disabled={disabled || isLoading}
115
+ onClick={handleInitialClick}
116
+ className={className}
117
+ {...props}
118
+ >
119
+ {children}
120
+ </Button>
121
+ );
122
+ }
123
+ );
124
+
125
+ ConfirmButton.displayName = 'ConfirmButton';
@@ -0,0 +1,43 @@
1
+ import * as React from 'react';
2
+ import { Eye, EyeOff } from 'lucide-react';
3
+ import { Input, type InputProps } from '../atoms/Input';
4
+ import { cn } from '../../infrastructure/utils';
5
+
6
+ export interface PasswordInputProps extends Omit<InputProps, 'type'> {
7
+ showToggle?: boolean;
8
+ }
9
+
10
+ export const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
11
+ ({ showToggle = true, className, ...props }, ref) => {
12
+ const [visible, setVisible] = React.useState(false);
13
+
14
+ const toggleVisibility = (e: React.MouseEvent) => {
15
+ e.preventDefault();
16
+ e.stopPropagation();
17
+ setVisible((prev) => !prev);
18
+ };
19
+
20
+ return (
21
+ <div className={cn('relative flex items-center w-full', className)}>
22
+ <Input
23
+ ref={ref}
24
+ type={visible ? 'text' : 'password'}
25
+ className="pr-10"
26
+ {...props}
27
+ />
28
+ {showToggle && (
29
+ <button
30
+ type="button"
31
+ onClick={toggleVisibility}
32
+ aria-label={visible ? 'Şifreyi gizle' : 'Şifreyi göster'}
33
+ className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground focus:outline-none transition-colors p-1"
34
+ >
35
+ {visible ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
36
+ </button>
37
+ )}
38
+ </div>
39
+ );
40
+ }
41
+ );
42
+
43
+ PasswordInput.displayName = 'PasswordInput';
@@ -0,0 +1,110 @@
1
+ import * as React from 'react';
2
+ import { Search, X } from 'lucide-react';
3
+ import { Input, type InputProps } from '../atoms/Input';
4
+ import { Spinner } from '../atoms/Spinner';
5
+ import { cn } from '../../infrastructure/utils';
6
+
7
+ export interface SearchInputProps extends Omit<InputProps, 'onChange' | 'value' | 'defaultValue'> {
8
+ value?: string;
9
+ defaultValue?: string;
10
+ onChange?: (value: string) => void;
11
+ onSearch?: (value: string) => void;
12
+ onClear?: () => void;
13
+ debounceMs?: number;
14
+ loading?: boolean;
15
+ clearable?: boolean;
16
+ }
17
+
18
+ export const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
19
+ (
20
+ {
21
+ value: controlledValue,
22
+ defaultValue = '',
23
+ onChange,
24
+ onSearch,
25
+ onClear,
26
+ debounceMs = 0,
27
+ loading = false,
28
+ clearable = true,
29
+ placeholder = 'Ara...',
30
+ className,
31
+ ...props
32
+ },
33
+ ref
34
+ ) => {
35
+ const isControlled = controlledValue !== undefined;
36
+ const [internalValue, setInternalValue] = React.useState(defaultValue);
37
+ const currentValue = isControlled ? controlledValue : internalValue;
38
+
39
+ const debounceTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
40
+
41
+ React.useEffect(() => {
42
+ return () => {
43
+ if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
44
+ };
45
+ }, []);
46
+
47
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
48
+ const nextVal = e.target.value;
49
+ if (!isControlled) {
50
+ setInternalValue(nextVal);
51
+ }
52
+ onChange?.(nextVal);
53
+
54
+ if (debounceMs > 0 && onSearch) {
55
+ if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
56
+ debounceTimerRef.current = setTimeout(() => {
57
+ onSearch(nextVal);
58
+ }, debounceMs);
59
+ }
60
+ };
61
+
62
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
63
+ if (e.key === 'Enter') {
64
+ if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
65
+ onSearch?.(currentValue);
66
+ }
67
+ };
68
+
69
+ const handleClear = () => {
70
+ if (!isControlled) {
71
+ setInternalValue('');
72
+ }
73
+ onChange?.('');
74
+ onClear?.();
75
+ onSearch?.('');
76
+ };
77
+
78
+ return (
79
+ <div className={cn('relative flex items-center w-full', className)}>
80
+ <Search className="absolute left-3 size-4 text-muted-foreground pointer-events-none shrink-0" />
81
+ <Input
82
+ ref={ref}
83
+ type="text"
84
+ value={currentValue}
85
+ onChange={handleChange}
86
+ onKeyDown={handleKeyDown}
87
+ placeholder={placeholder}
88
+ className="pl-9 pr-9"
89
+ {...props}
90
+ />
91
+ <div className="absolute right-2.5 flex items-center">
92
+ {loading ? (
93
+ <Spinner size="sm" />
94
+ ) : clearable && currentValue ? (
95
+ <button
96
+ type="button"
97
+ aria-label="Aramayı temizle"
98
+ onClick={handleClear}
99
+ className="text-muted-foreground hover:text-foreground transition-colors p-1"
100
+ >
101
+ <X className="size-3.5" />
102
+ </button>
103
+ ) : null}
104
+ </div>
105
+ </div>
106
+ );
107
+ }
108
+ );
109
+
110
+ SearchInput.displayName = 'SearchInput';
@@ -0,0 +1,36 @@
1
+ import * as React from 'react';
2
+ import { cn } from '../../infrastructure/utils';
3
+
4
+ export interface ToolbarProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ search?: React.ReactNode;
6
+ filters?: React.ReactNode;
7
+ actions?: React.ReactNode;
8
+ }
9
+
10
+ export const Toolbar = React.forwardRef<HTMLDivElement, ToolbarProps>(
11
+ ({ search, filters, actions, children, className, ...props }, ref) => {
12
+ return (
13
+ <div
14
+ ref={ref}
15
+ className={cn(
16
+ 'flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3 w-full',
17
+ className
18
+ )}
19
+ {...props}
20
+ >
21
+ <div className="flex flex-1 flex-wrap items-center gap-2">
22
+ {search && <div className="w-full sm:w-72 max-w-full">{search}</div>}
23
+ {filters && <div className="flex flex-wrap items-center gap-2">{filters}</div>}
24
+ {children}
25
+ </div>
26
+ {actions && (
27
+ <div className="flex items-center gap-2 shrink-0 justify-end">
28
+ {actions}
29
+ </div>
30
+ )}
31
+ </div>
32
+ );
33
+ }
34
+ );
35
+
36
+ Toolbar.displayName = 'Toolbar';