@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/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/__tests__/productivity-components.test.tsx +135 -0
- package/src/index.ts +84 -10
- package/src/presentation/atoms/CopyButton.tsx +93 -0
- package/src/presentation/atoms/StatusDot.tsx +79 -0
- package/src/presentation/atoms/index.ts +9 -4
- package/src/presentation/molecules/ConfirmButton.tsx +125 -0
- package/src/presentation/molecules/PasswordInput.tsx +43 -0
- package/src/presentation/molecules/SearchInput.tsx +110 -0
- package/src/presentation/molecules/Toolbar.tsx +36 -0
- package/src/presentation/molecules/index.ts +25 -2
- package/src/presentation/organisms/DescriptionList.tsx +113 -0
- package/src/presentation/organisms/StatsGrid.tsx +110 -0
- package/src/presentation/organisms/index.ts +10 -6
- package/src/presentation/templates/CrudPageTemplate.tsx +69 -0
- package/src/presentation/templates/SettingsLayout.tsx +102 -0
- package/src/presentation/templates/index.ts +7 -1
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 {
|
|
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 {
|
|
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
|
|
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';
|