@umituz/web-design-system 1.0.2 → 1.0.3

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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/domain/tokens/animation.tokens.ts +87 -0
  3. package/src/domain/tokens/index.ts +11 -0
  4. package/src/presentation/atoms/Checkbox.tsx +50 -0
  5. package/src/presentation/atoms/Divider.tsx +34 -0
  6. package/src/presentation/atoms/Link.tsx +45 -0
  7. package/src/presentation/atoms/Progress.tsx +48 -0
  8. package/src/presentation/atoms/Radio.tsx +42 -0
  9. package/src/presentation/atoms/Skeleton.tsx +38 -0
  10. package/src/presentation/atoms/Slider.tsx +49 -0
  11. package/src/presentation/atoms/Tooltip.tsx +76 -0
  12. package/src/presentation/atoms/index.ts +24 -0
  13. package/src/presentation/hooks/index.ts +13 -0
  14. package/src/presentation/hooks/useClickOutside.ts +33 -0
  15. package/src/presentation/hooks/useClipboard.ts +38 -0
  16. package/src/presentation/hooks/useDebounce.ts +22 -0
  17. package/src/presentation/hooks/useKeyboard.ts +89 -0
  18. package/src/presentation/hooks/useScrollLock.ts +26 -0
  19. package/src/presentation/hooks/useToggle.ts +16 -0
  20. package/src/presentation/molecules/CheckboxGroup.tsx +68 -0
  21. package/src/presentation/molecules/InputGroup.tsx +65 -0
  22. package/src/presentation/molecules/RadioGroup.tsx +63 -0
  23. package/src/presentation/molecules/Select.tsx +41 -0
  24. package/src/presentation/molecules/Textarea.tsx +43 -0
  25. package/src/presentation/molecules/index.ts +15 -0
  26. package/src/presentation/organisms/Accordion.tsx +117 -0
  27. package/src/presentation/organisms/Breadcrumb.tsx +83 -0
  28. package/src/presentation/organisms/Table.tsx +120 -0
  29. package/src/presentation/organisms/Tabs.tsx +99 -0
  30. package/src/presentation/organisms/index.ts +21 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * useKeyboard Hook
3
+ * @description Keyboard event handling
4
+ */
5
+
6
+ import { useEffect } from 'react';
7
+
8
+ export type KeyboardKey = string;
9
+ export type KeyboardModifier = 'ctrl' | 'shift' | 'alt' | 'meta';
10
+
11
+ export interface KeyboardOptions {
12
+ key: KeyboardKey;
13
+ modifiers?: KeyboardModifier[];
14
+ onKeyDown?: (event: KeyboardEvent) => void;
15
+ onKeyUp?: (event: KeyboardEvent) => void;
16
+ enabled?: boolean;
17
+ }
18
+
19
+ export function useKeyboard({
20
+ key,
21
+ modifiers = [],
22
+ onKeyDown,
23
+ onKeyUp,
24
+ enabled = true,
25
+ }: KeyboardOptions) {
26
+ useEffect(() => {
27
+ if (!enabled) return;
28
+
29
+ const handleKeyDown = (event: KeyboardEvent) => {
30
+ const keyMatches = event.key.toLowerCase() === key.toLowerCase();
31
+ const modifiersMatch = modifiers.every((mod) => {
32
+ switch (mod) {
33
+ case 'ctrl':
34
+ return event.ctrlKey;
35
+ case 'shift':
36
+ return event.shiftKey;
37
+ case 'alt':
38
+ return event.altKey;
39
+ case 'meta':
40
+ return event.metaKey;
41
+ default:
42
+ return true;
43
+ }
44
+ });
45
+
46
+ if (keyMatches && modifiersMatch) {
47
+ onKeyDown?.(event);
48
+ }
49
+ };
50
+
51
+ const handleKeyUp = (event: KeyboardEvent) => {
52
+ const keyMatches = event.key.toLowerCase() === key.toLowerCase();
53
+ const modifiersMatch = modifiers.every((mod) => {
54
+ switch (mod) {
55
+ case 'ctrl':
56
+ return event.ctrlKey;
57
+ case 'shift':
58
+ return event.shiftKey;
59
+ case 'alt':
60
+ return event.altKey;
61
+ case 'meta':
62
+ return event.metaKey;
63
+ default:
64
+ return true;
65
+ }
66
+ });
67
+
68
+ if (keyMatches && modifiersMatch) {
69
+ onKeyUp?.(event);
70
+ }
71
+ };
72
+
73
+ document.addEventListener('keydown', handleKeyDown);
74
+ document.addEventListener('keyup', handleKeyUp);
75
+
76
+ return () => {
77
+ document.removeEventListener('keydown', handleKeyDown);
78
+ document.removeEventListener('keyup', handleKeyUp);
79
+ };
80
+ }, [key, modifiers, onKeyDown, onKeyUp, enabled]);
81
+ }
82
+
83
+ export function useEscape(callback: () => void, enabled = true) {
84
+ useKeyboard({
85
+ key: 'Escape',
86
+ onKeyDown: callback,
87
+ enabled,
88
+ });
89
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * useScrollLock Hook
3
+ * @description Lock/unlock body scroll
4
+ */
5
+
6
+ import { useEffect } from 'react';
7
+
8
+ export function useScrollLock(enabled: boolean = true) {
9
+ useEffect(() => {
10
+ if (!enabled) return;
11
+
12
+ const originalStyle = window.getComputedStyle(document.body).overflow;
13
+ const originalPaddingRight = window.getComputedStyle(document.body).paddingRight;
14
+
15
+ // Calculate scrollbar width
16
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
17
+
18
+ document.body.style.overflow = 'hidden';
19
+ document.body.style.paddingRight = `${scrollbarWidth}px`;
20
+
21
+ return () => {
22
+ document.body.style.overflow = originalStyle;
23
+ document.body.style.paddingRight = originalPaddingRight;
24
+ };
25
+ }, [enabled]);
26
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * useToggle Hook
3
+ * @description Toggle boolean state
4
+ */
5
+
6
+ import { useCallback, useState } from 'react';
7
+
8
+ export function useToggle(initialValue: boolean = false): [boolean, () => void, (value: boolean) => void] {
9
+ const [value, setValue] = useState(initialValue);
10
+
11
+ const toggle = useCallback(() => {
12
+ setValue((v) => !v);
13
+ }, []);
14
+
15
+ return [value, toggle, setValue];
16
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * CheckboxGroup Component (Molecule)
3
+ * @description Group of checkboxes
4
+ */
5
+
6
+ import { forwardRef, type HTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps } from '../../domain/types';
9
+ import { Checkbox } from '../atoms/Checkbox';
10
+ import { Text } from '../atoms/Text';
11
+
12
+ export interface CheckboxOption {
13
+ value: string;
14
+ label: string;
15
+ disabled?: boolean;
16
+ }
17
+
18
+ export interface CheckboxGroupProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
19
+ options: CheckboxOption[];
20
+ value?: string[];
21
+ onChange?: (values: string[]) => void;
22
+ orientation?: 'vertical' | 'horizontal';
23
+ }
24
+
25
+ const orientationStyles: Record<'vertical' | 'horizontal', string> = {
26
+ vertical: 'flex-col gap-3',
27
+ horizontal: 'flex-row gap-6',
28
+ };
29
+
30
+ export const CheckboxGroup = forwardRef<HTMLDivElement, CheckboxGroupProps>(
31
+ ({ className, options, value = [], onChange, orientation = 'vertical', ...props }, ref) => {
32
+ const handleCheckedChange = (optionValue: string, checked: boolean) => {
33
+ if (checked) {
34
+ onChange?.([...value, optionValue]);
35
+ } else {
36
+ onChange?.(value.filter((v) => v !== optionValue));
37
+ }
38
+ };
39
+
40
+ return (
41
+ <div
42
+ ref={ref}
43
+ role="group"
44
+ className={cn('flex', orientationStyles[orientation], className)}
45
+ {...props}
46
+ >
47
+ {options.map((option) => (
48
+ <label
49
+ key={option.value}
50
+ className={cn(
51
+ 'flex items-center gap-2 cursor-pointer',
52
+ option.disabled && 'opacity-50 cursor-not-allowed'
53
+ )}
54
+ >
55
+ <Checkbox
56
+ checked={value.includes(option.value)}
57
+ onCheckedChange={(checked) => handleCheckedChange(option.value, checked)}
58
+ disabled={option.disabled}
59
+ />
60
+ <Text size="sm">{option.label}</Text>
61
+ </label>
62
+ ))}
63
+ </div>
64
+ );
65
+ }
66
+ );
67
+
68
+ CheckboxGroup.displayName = 'CheckboxGroup';
@@ -0,0 +1,65 @@
1
+ /**
2
+ * InputGroup Component (Molecule)
3
+ * @description Input with prefix/suffix elements
4
+ */
5
+
6
+ import { forwardRef, type ReactNode, type HTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps, ChildrenProps } from '../../domain/types';
9
+ import { Input } from '../atoms/Input';
10
+
11
+ export interface InputGroupProps extends HTMLAttributes<HTMLDivElement>, BaseProps, ChildrenProps {
12
+ leftElement?: ReactNode;
13
+ rightElement?: ReactNode;
14
+ }
15
+
16
+ export const InputGroup = forwardRef<HTMLDivElement, InputGroupProps>(
17
+ ({ className, leftElement, rightElement, children, ...props }, ref) => {
18
+ return (
19
+ <div
20
+ ref={ref}
21
+ className="relative"
22
+ {...props}
23
+ >
24
+ {leftElement && (
25
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
26
+ {leftElement}
27
+ </div>
28
+ )}
29
+ {children}
30
+ {rightElement && (
31
+ <div className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground">
32
+ {rightElement}
33
+ </div>
34
+ )}
35
+ </div>
36
+ );
37
+ }
38
+ );
39
+
40
+ InputGroup.displayName = 'InputGroup';
41
+
42
+ export const GroupedInput = forwardRef<HTMLInputElement, React.ComponentProps<typeof Input> & {
43
+ leftElement?: ReactNode;
44
+ rightElement?: ReactNode;
45
+ }>(
46
+ ({ className, leftElement, rightElement, ...props }, ref) => {
47
+ return (
48
+ <InputGroup>
49
+ {leftElement}
50
+ <Input
51
+ ref={ref}
52
+ className={cn(
53
+ leftElement && 'pl-10',
54
+ rightElement && 'pr-10',
55
+ className
56
+ )}
57
+ {...props}
58
+ />
59
+ {rightElement}
60
+ </InputGroup>
61
+ );
62
+ }
63
+ );
64
+
65
+ GroupedInput.displayName = 'GroupedInput';
@@ -0,0 +1,63 @@
1
+ /**
2
+ * RadioGroup Component (Molecule)
3
+ * @description Group of radio buttons
4
+ */
5
+
6
+ import { forwardRef, type HTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps } from '../../domain/types';
9
+ import { Radio } from '../atoms/Radio';
10
+ import { Text } from '../atoms/Text';
11
+
12
+ export interface RadioOption {
13
+ value: string;
14
+ label: string;
15
+ disabled?: boolean;
16
+ }
17
+
18
+ export interface RadioGroupProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
19
+ name: string;
20
+ options: RadioOption[];
21
+ value?: string;
22
+ onChange?: (value: string) => void;
23
+ orientation?: 'vertical' | 'horizontal';
24
+ }
25
+
26
+ const orientationStyles: Record<'vertical' | 'horizontal', string> = {
27
+ vertical: 'flex-col gap-3',
28
+ horizontal: 'flex-row gap-6',
29
+ };
30
+
31
+ export const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
32
+ ({ className, name, options, value, onChange, orientation = 'vertical', ...props }, ref) => {
33
+ return (
34
+ <div
35
+ ref={ref}
36
+ role="radiogroup"
37
+ className={cn('flex', orientationStyles[orientation], className)}
38
+ {...props}
39
+ >
40
+ {options.map((option) => (
41
+ <label
42
+ key={option.value}
43
+ className={cn(
44
+ 'flex items-center gap-2 cursor-pointer',
45
+ option.disabled && 'opacity-50 cursor-not-allowed'
46
+ )}
47
+ >
48
+ <Radio
49
+ name={name}
50
+ value={option.value}
51
+ checked={value === option.value}
52
+ onChange={() => !option.disabled && onChange?.(option.value)}
53
+ disabled={option.disabled}
54
+ />
55
+ <Text size="sm">{option.label}</Text>
56
+ </label>
57
+ ))}
58
+ </div>
59
+ );
60
+ }
61
+ );
62
+
63
+ RadioGroup.displayName = 'RadioGroup';
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Select Component (Molecule)
3
+ * @description Dropdown select input
4
+ */
5
+
6
+ import { forwardRef, type SelectHTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps } from '../../domain/types';
9
+
10
+ export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement>, BaseProps {
11
+ error?: boolean;
12
+ options: Array<{ value: string; label: string; disabled?: boolean }>;
13
+ }
14
+
15
+ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
16
+ ({ className, error, options, disabled, ...props }, ref) => {
17
+ return (
18
+ <select
19
+ ref={ref}
20
+ disabled={disabled}
21
+ className={cn(
22
+ 'flex h-9 w-full rounded-md border border-input bg-background px-3 py-2',
23
+ 'text-sm ring-offset-background',
24
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
25
+ 'disabled:cursor-not-allowed disabled:opacity-50',
26
+ error && 'border-destructive',
27
+ className
28
+ )}
29
+ {...props}
30
+ >
31
+ {options.map((option) => (
32
+ <option key={option.value} value={option.value} disabled={option.disabled}>
33
+ {option.label}
34
+ </option>
35
+ ))}
36
+ </select>
37
+ );
38
+ }
39
+ );
40
+
41
+ Select.displayName = 'Select';
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Textarea Component (Molecule)
3
+ * @description Multi-line text input
4
+ */
5
+
6
+ import { forwardRef, type TextareaHTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps } from '../../domain/types';
9
+
10
+ export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement>, BaseProps {
11
+ error?: boolean;
12
+ resize?: 'none' | 'both' | 'horizontal' | 'vertical';
13
+ }
14
+
15
+ const resizeStyles: Record<'none' | 'both' | 'horizontal' | 'vertical', string> = {
16
+ none: 'resize-none',
17
+ both: 'resize',
18
+ horizontal: 'resize-x',
19
+ vertical: 'resize-y',
20
+ };
21
+
22
+ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
23
+ ({ className, error, resize = 'vertical', ...props }, ref) => {
24
+ return (
25
+ <textarea
26
+ ref={ref}
27
+ className={cn(
28
+ 'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2',
29
+ 'text-sm ring-offset-background',
30
+ 'placeholder:text-muted-foreground',
31
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
32
+ 'disabled:cursor-not-allowed disabled:opacity-50',
33
+ error && 'border-destructive',
34
+ resizeStyles[resize],
35
+ className
36
+ )}
37
+ {...props}
38
+ />
39
+ );
40
+ }
41
+ );
42
+
43
+ Textarea.displayName = 'Textarea';
@@ -18,3 +18,18 @@ export type { ChipProps } from './Chip';
18
18
 
19
19
  export { Toggle } from './Toggle';
20
20
  export type { ToggleProps } from './Toggle';
21
+
22
+ export { Select } from './Select';
23
+ export type { SelectProps } from './Select';
24
+
25
+ export { Textarea } from './Textarea';
26
+ export type { TextareaProps } from './Textarea';
27
+
28
+ export { RadioGroup } from './RadioGroup';
29
+ export type { RadioGroupProps, RadioOption } from './RadioGroup';
30
+
31
+ export { CheckboxGroup } from './CheckboxGroup';
32
+ export type { CheckboxGroupProps, CheckboxOption } from './CheckboxGroup';
33
+
34
+ export { InputGroup, GroupedInput } from './InputGroup';
35
+ export type { InputGroupProps } from './InputGroup';
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Accordion Component (Organism)
3
+ * @description Collapsible content sections
4
+ */
5
+
6
+ import { useState, useCallback, type ReactNode, type HTMLAttributes } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps } from '../../domain/types';
9
+ import { Icon } from '../atoms/Icon';
10
+
11
+ export interface AccordionItem {
12
+ value: string;
13
+ title: string;
14
+ content: ReactNode;
15
+ disabled?: boolean;
16
+ }
17
+
18
+ export interface AccordionProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
19
+ items: AccordionItem[];
20
+ allowMultiple?: boolean;
21
+ defaultValue?: string[];
22
+ variant?: 'default' | 'bordered' | 'ghost';
23
+ }
24
+
25
+ const variantStyles: Record<'default' | 'bordered' | 'ghost', string> = {
26
+ default: 'border-b',
27
+ bordered: 'border rounded-lg mb-2',
28
+ ghost: 'border-0',
29
+ };
30
+
31
+ export function Accordion({
32
+ items,
33
+ allowMultiple = false,
34
+ defaultValue = [],
35
+ variant = 'default',
36
+ className,
37
+ ...props
38
+ }: AccordionProps) {
39
+ const [openItems, setOpenItems] = useState<string[]>(defaultValue);
40
+
41
+ const toggleItem = useCallback((value: string) => {
42
+ setOpenItems((prev) => {
43
+ const isOpen = prev.includes(value);
44
+
45
+ if (allowMultiple) {
46
+ return isOpen
47
+ ? prev.filter((v) => v !== value)
48
+ : [...prev, value];
49
+ } else {
50
+ return isOpen ? [] : [value];
51
+ }
52
+ });
53
+ }, [allowMultiple]);
54
+
55
+ return (
56
+ <div className={cn('w-full', className)} {...props}>
57
+ {items.map((item, index) => {
58
+ const isOpen = openItems.includes(item.value);
59
+
60
+ return (
61
+ <div
62
+ key={item.value}
63
+ className={cn(
64
+ 'group',
65
+ variantStyles[variant],
66
+ variant === 'bordered' && isOpen && 'ring-1 ring-ring'
67
+ )}
68
+ >
69
+ {/* Header */}
70
+ <button
71
+ onClick={() => toggleItem(item.value)}
72
+ disabled={item.disabled}
73
+ className={cn(
74
+ 'flex w-full items-center justify-between py-4 font-medium transition-all',
75
+ 'hover:text-foreground',
76
+ item.disabled && 'opacity-50 cursor-not-allowed',
77
+ variant === 'bordered' && 'px-4'
78
+ )}
79
+ >
80
+ <span>{item.title}</span>
81
+ <Icon
82
+ className={cn(
83
+ 'transition-transform duration-200',
84
+ isOpen && 'rotate-180'
85
+ )}
86
+ size="sm"
87
+ >
88
+ <path
89
+ strokeLinecap="round"
90
+ strokeLinejoin="round"
91
+ d="M19.5 8.25l-7.5 7.5-7.5-7.5"
92
+ />
93
+ </Icon>
94
+ </button>
95
+
96
+ {/* Content */}
97
+ {isOpen && (
98
+ <div
99
+ className={cn(
100
+ 'overflow-hidden',
101
+ 'animate-accordion-down',
102
+ variant === 'bordered' && 'px-4 pb-4'
103
+ )}
104
+ >
105
+ <div className="pb-4 text-sm text-muted-foreground">
106
+ {item.content}
107
+ </div>
108
+ </div>
109
+ )}
110
+ </div>
111
+ );
112
+ })}
113
+ </div>
114
+ );
115
+ }
116
+
117
+ Accordion.displayName = 'Accordion';
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Breadcrumb Component (Organism)
3
+ * @description Navigation breadcrumb trail
4
+ */
5
+
6
+ import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import type { BaseProps } from '../../domain/types';
9
+ import { Icon } from '../atoms/Icon';
10
+ import { Link } from '../atoms/Link';
11
+
12
+ export interface BreadcrumbItem {
13
+ label: string;
14
+ href?: string;
15
+ icon?: ReactNode;
16
+ }
17
+
18
+ export interface BreadcrumbsProps extends HTMLAttributes<HTMLElement>, BaseProps {
19
+ items: BreadcrumbItem[];
20
+ separator?: ReactNode;
21
+ }
22
+
23
+ export const Breadcrumbs = forwardRef<HTMLElement, BreadcrumbsProps>(
24
+ ({ className, items, separator, ...props }, ref) => {
25
+ const defaultSeparator = (
26
+ <Icon size="xs" className="text-muted-foreground">
27
+ <path
28
+ strokeLinecap="round"
29
+ strokeLinejoin="round"
30
+ d="M8.25 4.5l7.5 7.5-7.5 7.5"
31
+ />
32
+ </Icon>
33
+ );
34
+
35
+ return (
36
+ <nav
37
+ ref={ref}
38
+ aria-label="Breadcrumb"
39
+ className={cn('flex items-center gap-2 text-sm', className)}
40
+ {...props}
41
+ >
42
+ {items.map((item, index) => {
43
+ const isLast = index === items.length - 1;
44
+
45
+ return (
46
+ <div key={index} className="flex items-center gap-2">
47
+ {index > 0 && (separator || defaultSeparator)}
48
+
49
+ {item.href ? (
50
+ <Link
51
+ href={item.href}
52
+ className={cn(
53
+ 'flex items-center gap-1.5',
54
+ isLast
55
+ ? 'text-foreground font-medium'
56
+ : 'text-muted-foreground hover:text-foreground'
57
+ )}
58
+ >
59
+ {item.icon}
60
+ {item.label}
61
+ </Link>
62
+ ) : (
63
+ <span
64
+ className={cn(
65
+ 'flex items-center gap-1.5',
66
+ isLast
67
+ ? 'text-foreground font-medium'
68
+ : 'text-muted-foreground'
69
+ )}
70
+ >
71
+ {item.icon}
72
+ {item.label}
73
+ </span>
74
+ )}
75
+ </div>
76
+ );
77
+ })}
78
+ </nav>
79
+ );
80
+ }
81
+ );
82
+
83
+ Breadcrumbs.displayName = 'Breadcrumbs';