podo-ui 0.3.6 → 0.3.8

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.
@@ -1,67 +0,0 @@
1
- import { useState, useEffect, useCallback } from 'react';
2
- import { z } from 'zod';
3
- import styles from './input.module.scss';
4
-
5
- interface InputWrapperProps extends React.ComponentProps<'input'> {
6
- value?: string | number;
7
- className?: string;
8
- validator?: z.ZodType<unknown>;
9
- withIcon?: string;
10
- withRightIcon?: string;
11
- unit?: string;
12
- }
13
-
14
- const Input: React.FC<InputWrapperProps> = ({
15
- validator,
16
- value,
17
- className,
18
- withIcon,
19
- withRightIcon,
20
- unit,
21
- ...rest
22
- }) => {
23
- const [message, setMessage] = useState('');
24
- const [statusClass, setStatusClass] = useState('');
25
-
26
- const validateHandler = useCallback(() => {
27
- setMessage('');
28
- setStatusClass('');
29
- if (validator && value) {
30
- try {
31
- validator.parse(value);
32
- setStatusClass('success');
33
- } catch (e) {
34
- if (e instanceof z.ZodError) {
35
- setMessage(e.errors[0].message);
36
- setStatusClass('danger');
37
- }
38
- }
39
- }
40
- }, [validator, value]);
41
-
42
- useEffect(() => {
43
- validateHandler();
44
- }, [validateHandler, value]);
45
-
46
- return (
47
- <div className={`${styles.style} ${className || ''}`}>
48
- <div
49
- className={`${className || ''} ${withIcon ? 'with-icon' : ''} ${withRightIcon ? 'with-right-icon' : ''}`}
50
- >
51
- {withIcon && <i className={withIcon} />}
52
- <input
53
- {...rest}
54
- value={value ?? ''}
55
- className={`${statusClass} ${className || ''}`}
56
- />
57
- {withRightIcon && <i className={withRightIcon} />}
58
- {unit && <span className="unit">{unit}</span>}
59
- </div>
60
- {validator && message !== '' && (
61
- <div className="validator">{message}</div>
62
- )}
63
- </div>
64
- );
65
- };
66
-
67
- export default Input;
@@ -1,51 +0,0 @@
1
- import { useState } from 'react';
2
- import { z } from 'zod';
3
- import styles from './textarea.module.scss';
4
-
5
- interface TextareaWrapperProps extends React.ComponentProps<'textarea'> {
6
- value: string;
7
- className?: string;
8
- validator?: z.ZodType<unknown>;
9
- }
10
-
11
- const Textarea: React.FC<TextareaWrapperProps> = ({
12
- validator,
13
- value,
14
- className,
15
- ...rest
16
- }) => {
17
- const [message, setMessage] = useState('');
18
- const [statusClass, setStatusClass] = useState('');
19
-
20
- const validateHandler = () => {
21
- setMessage('');
22
- setStatusClass('');
23
- if (validator && value.length > 0) {
24
- try {
25
- validator.parse(value);
26
- setStatusClass('success');
27
- } catch (e) {
28
- if (e instanceof z.ZodError) {
29
- setMessage(e.errors[0].message);
30
- setStatusClass('danger');
31
- }
32
- }
33
- }
34
- };
35
-
36
- return (
37
- <div className={`${styles.style} ${className}`}>
38
- <textarea
39
- {...rest}
40
- value={value}
41
- className={`${statusClass} ${className}`}
42
- onKeyUp={validateHandler}
43
- />
44
- {validator && message !== '' && (
45
- <div className="validator">{message}</div>
46
- )}
47
- </div>
48
- );
49
- };
50
-
51
- export default Textarea;
@@ -1,78 +0,0 @@
1
- import { z } from 'zod';
2
- import styles from './field.module.scss';
3
- import { useCallback, useEffect, useState } from 'react';
4
-
5
- interface Props {
6
- label?: string;
7
- labelClass?: string;
8
- required?: boolean;
9
- helper?: string;
10
- helperClass?: string;
11
- children?: React.ReactNode;
12
- validator?: z.ZodType<unknown>;
13
- value?: string;
14
- setClassName?: React.Dispatch<React.SetStateAction<string>>;
15
- className?: string;
16
- }
17
-
18
- const Field = ({
19
- label,
20
- labelClass,
21
- required,
22
- helper,
23
- helperClass,
24
- children,
25
- validator,
26
- value,
27
- setClassName,
28
- className,
29
- }: Props) => {
30
- const [message, setMessage] = useState('');
31
-
32
- const validateHandler = useCallback(() => {
33
- setMessage('');
34
- if (setClassName) {
35
- setClassName('');
36
- }
37
- if (validator && value && value.length > 0) {
38
- try {
39
- validator.parse(value);
40
- if (setClassName) {
41
- setClassName('success');
42
- }
43
- } catch (e) {
44
- if (e instanceof z.ZodError) {
45
- setMessage(e.errors[0].message);
46
- if (setClassName) {
47
- setClassName('danger');
48
- }
49
- }
50
- }
51
- }
52
- }, [validator, value, setClassName]);
53
-
54
- useEffect(() => {
55
- validateHandler();
56
- }, [validateHandler]);
57
-
58
- return (
59
- <div className={`${styles.style} ${className || ''}`}>
60
- {label && (
61
- <label className={labelClass}>
62
- {label}
63
-
64
- {required && <span className="required">*</span>}
65
- </label>
66
- )}
67
- <div className="child">{children}</div>
68
- {helper ||
69
- (validator && message !== '' && (
70
- <div className={`helper ${helperClass || ''}`}>
71
- {message || helper}
72
- </div>
73
- ))}
74
- </div>
75
- );
76
- };
77
-
78
- export default Field;
@@ -1,87 +0,0 @@
1
- import styles from './pagination.module.scss';
2
-
3
- interface PaginationProps {
4
- currentPage: number;
5
- totalPages: number;
6
- onPageChange: (page: number) => void;
7
- maxVisiblePages?: number;
8
- }
9
-
10
- export default function Pagination({
11
- currentPage,
12
- totalPages,
13
- onPageChange,
14
- maxVisiblePages = 5,
15
- }: PaginationProps) {
16
- const getPageNumbers = () => {
17
- const pages: number[] = [];
18
- const startPage = Math.floor((currentPage - 1) / maxVisiblePages) * maxVisiblePages + 1;
19
- const endPage = Math.min(startPage + maxVisiblePages - 1, totalPages);
20
-
21
- for (let i = startPage; i <= endPage; i++) {
22
- pages.push(i);
23
- }
24
-
25
- return pages;
26
- };
27
-
28
- const pageNumbers = getPageNumbers();
29
-
30
- const handlePrevious = () => {
31
- if (currentPage > 1) {
32
- onPageChange(currentPage - 1);
33
- }
34
- };
35
-
36
- const handleNext = () => {
37
- if (currentPage < totalPages) {
38
- onPageChange(currentPage + 1);
39
- }
40
- };
41
-
42
- if (totalPages === 0) {
43
- return null;
44
- }
45
-
46
- return (
47
- <div className={styles.pagination}>
48
- {currentPage > 1 ? (
49
- <button
50
- onClick={handlePrevious}
51
- className={styles.pageButton}
52
- aria-label="이전 페이지"
53
- >
54
- <i className="icon-arrow-left"></i>
55
- </button>
56
- ) : (
57
- <div className={styles.pageButtonPlaceholder} />
58
- )}
59
-
60
- {pageNumbers.map((pageNum) => (
61
- <button
62
- key={pageNum}
63
- onClick={() => onPageChange(pageNum)}
64
- className={`${styles.pageButton} ${
65
- currentPage === pageNum ? styles.active : ''
66
- }`}
67
- aria-label={`${pageNum}페이지`}
68
- aria-current={currentPage === pageNum ? 'page' : undefined}
69
- >
70
- {pageNum}
71
- </button>
72
- ))}
73
-
74
- {currentPage < totalPages ? (
75
- <button
76
- onClick={handleNext}
77
- className={styles.pageButton}
78
- aria-label="다음 페이지"
79
- >
80
- <i className="icon-arrow-right"></i>
81
- </button>
82
- ) : (
83
- <div className={styles.pageButtonPlaceholder} />
84
- )}
85
- </div>
86
- );
87
- }
@@ -1,123 +0,0 @@
1
- 'use client';
2
-
3
- import React, { createContext, useContext, useState, useCallback } from 'react';
4
- import { createPortal } from 'react-dom';
5
- import Toast, { ToastProps, ToastPosition, ToastVariant } from './toast';
6
- import styles from './toast-container.module.scss';
7
-
8
- interface ToastData extends Omit<ToastProps, 'id' | 'onClose'> {
9
- id: string;
10
- position: ToastPosition;
11
- }
12
-
13
- interface ToastOptions {
14
- header?: string;
15
- message: string;
16
- variant?: ToastVariant;
17
- type?: 'type01' | 'type02';
18
- long?: boolean;
19
- duration?: number;
20
- width?: string | number;
21
- position?: ToastPosition;
22
- }
23
-
24
- interface ToastContextType {
25
- showToast: (options: ToastOptions) => string;
26
- hideToast: (id: string) => void;
27
- }
28
-
29
- const ToastContext = createContext<ToastContextType | undefined>(undefined);
30
-
31
- export const useToast = () => {
32
- const context = useContext(ToastContext);
33
- if (!context) {
34
- throw new Error('useToast must be used within ToastProvider');
35
- }
36
- return context;
37
- };
38
-
39
- export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
40
- const [toasts, setToasts] = useState<ToastData[]>([]);
41
- const [isMounted, setIsMounted] = useState(false);
42
-
43
- React.useEffect(() => {
44
- setIsMounted(true);
45
- }, []);
46
-
47
- const showToast = useCallback((options: ToastOptions): string => {
48
- const id = `toast-${Date.now()}-${Math.random()}`;
49
- const position = options.position || 'top-right';
50
-
51
- setToasts((prev) => [
52
- ...prev,
53
- {
54
- id,
55
- position,
56
- header: options.header,
57
- message: options.message,
58
- variant: options.variant,
59
- type: options.type,
60
- long: options.long,
61
- duration: options.duration,
62
- width: options.width,
63
- },
64
- ]);
65
-
66
- return id;
67
- }, []);
68
-
69
- const hideToast = useCallback((id: string) => {
70
- setToasts((prev) => prev.filter((toast) => toast.id !== id));
71
- }, []);
72
-
73
- const getToastsByPosition = (position: ToastPosition) => {
74
- return toasts.filter((toast) => toast.position === position);
75
- };
76
-
77
- const positions: ToastPosition[] = [
78
- 'top-left',
79
- 'top-center',
80
- 'top-right',
81
- 'center-left',
82
- 'center',
83
- 'center-right',
84
- 'bottom-left',
85
- 'bottom-center',
86
- 'bottom-right',
87
- ];
88
-
89
- return (
90
- <ToastContext.Provider value={{ showToast, hideToast }}>
91
- {children}
92
- {isMounted &&
93
- createPortal(
94
- <div className={styles.toastPortal}>
95
- {positions.map((position) => {
96
- const positionToasts = getToastsByPosition(position);
97
- if (positionToasts.length === 0) return null;
98
-
99
- return (
100
- <div key={position} className={`${styles.toastContainer} ${styles[position]}`}>
101
- {positionToasts.map((toast) => (
102
- <Toast
103
- key={toast.id}
104
- id={toast.id}
105
- header={toast.header}
106
- message={toast.message}
107
- variant={toast.variant}
108
- type={toast.type}
109
- long={toast.long}
110
- duration={toast.duration}
111
- width={toast.width}
112
- onClose={hideToast}
113
- />
114
- ))}
115
- </div>
116
- );
117
- })}
118
- </div>,
119
- document.body
120
- )}
121
- </ToastContext.Provider>
122
- );
123
- };
@@ -1,116 +0,0 @@
1
- 'use client';
2
-
3
- import { useEffect, useState } from 'react';
4
- import styles from './toast.module.scss';
5
-
6
- export type ToastPosition =
7
- | 'top-left'
8
- | 'top-center'
9
- | 'top-right'
10
- | 'center-left'
11
- | 'center'
12
- | 'center-right'
13
- | 'bottom-left'
14
- | 'bottom-center'
15
- | 'bottom-right';
16
-
17
- export type ToastVariant = 'default' | 'primary' | 'info' | 'success' | 'warning' | 'danger';
18
-
19
- export interface ToastProps {
20
- id: string;
21
- header?: string;
22
- message: string;
23
- variant?: ToastVariant;
24
- type?: 'type01' | 'type02';
25
- long?: boolean;
26
- duration?: number;
27
- width?: string | number;
28
- onClose: (id: string) => void;
29
- }
30
-
31
- const Toast: React.FC<ToastProps> = ({
32
- id,
33
- header,
34
- message,
35
- variant = 'default',
36
- type = 'type01',
37
- long = false,
38
- duration = 3000,
39
- width,
40
- onClose,
41
- }) => {
42
- const [isVisible, setIsVisible] = useState(false);
43
- const [isClosing, setIsClosing] = useState(false);
44
-
45
- useEffect(() => {
46
- // Fade in
47
- requestAnimationFrame(() => {
48
- setIsVisible(true);
49
- });
50
-
51
- // Auto close
52
- if (duration > 0) {
53
- const timer = setTimeout(() => {
54
- handleClose();
55
- }, duration);
56
-
57
- return () => clearTimeout(timer);
58
- }
59
- }, [duration]);
60
-
61
- const handleClose = () => {
62
- setIsClosing(true);
63
- setTimeout(() => {
64
- onClose(id);
65
- }, 200); // 0.2s fade out
66
- };
67
-
68
- const toastClasses = [
69
- 'toast',
70
- variant,
71
- type === 'type02' ? 'toast-border' : '',
72
- long ? 'toast-long' : '',
73
- styles.toastAnimation,
74
- isVisible && !isClosing ? styles.fadeIn : '',
75
- isClosing ? styles.fadeOut : '',
76
- ]
77
- .filter(Boolean)
78
- .join(' ');
79
-
80
- const toastStyle: React.CSSProperties = {
81
- width: width ? (typeof width === 'number' ? `${width}px` : width) : 'auto',
82
- };
83
-
84
- const getIcon = () => {
85
- switch (variant) {
86
- case 'success':
87
- return 'icon-check';
88
- case 'warning':
89
- return 'icon-warning';
90
- case 'danger':
91
- return 'icon-danger';
92
- case 'primary':
93
- case 'info':
94
- case 'default':
95
- default:
96
- return 'icon-info';
97
- }
98
- };
99
-
100
- return (
101
- <div className={toastClasses} style={toastStyle}>
102
- <div className="toast-icon">
103
- <i className={getIcon()}></i>
104
- </div>
105
- <div className="toast-content">
106
- {header && !long && <div className="toast-header">{header}</div>}
107
- <div className="toast-body">{message}</div>
108
- </div>
109
- <button className="toast-close" onClick={handleClose}>
110
- <i className="icon-close"></i>
111
- </button>
112
- </div>
113
- );
114
- };
115
-
116
- export default Toast;
package/react.ts DELETED
@@ -1,17 +0,0 @@
1
- import Input from './react/atom/input';
2
- import Textarea from './react/atom/textarea';
3
- import Editor from './react/atom/editor';
4
- import EditorView from './react/atom/editor-view';
5
- import Pagination from './react/molecule/pagination';
6
- import Field from './react/molecule/field';
7
- const Form = {
8
- Input,
9
- Textarea,
10
- Editor,
11
- EditorView,
12
- Field,
13
- };
14
-
15
- export default Form;
16
-
17
- export { Input, Textarea, Editor, EditorView, Pagination, Field };
package/system.scss DELETED
File without changes