neogestify-ui-components 1.0.0 → 1.1.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.
@@ -0,0 +1,72 @@
1
+ import { AnimateSpin } from '../icons/icons';
2
+ import { type ButtonHTMLAttributes, type FC, type ReactNode } from 'react';
3
+
4
+ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
5
+ variant?: 'primary' | 'secondary' | 'icon' | 'danger' | 'success' | 'outline' | 'nav' | 'custom' | 'link' | 'warning' | 'toggle';
6
+ children: ReactNode;
7
+ isLoading?: boolean;
8
+ loadingText?: string;
9
+ isActive?: boolean;
10
+ }
11
+
12
+ export const Button: FC<ButtonProps> = ({
13
+ variant = 'primary',
14
+ children,
15
+ isLoading = false,
16
+ loadingText,
17
+ isActive = false,
18
+ className = '',
19
+ disabled,
20
+ ...props
21
+ }) => {
22
+ const baseClasses = 'transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed hover:cursor-pointer';
23
+
24
+ const variantClasses = {
25
+ primary: 'py-2 px-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 dark:bg-indigo-500 dark:hover:bg-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-400 focus:ring-offset-gray-50 dark:focus:ring-offset-gray-900',
26
+ secondary: 'p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-800 rounded-md border border-gray-300 dark:border-gray-600 shadow-sm hover:shadow-md focus:ring-indigo-500 focus:ring-offset-gray-50 dark:focus:ring-offset-gray-900',
27
+ icon: 'p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-800 rounded-full focus:ring-indigo-500 focus:ring-offset-gray-50 dark:focus:ring-offset-gray-900',
28
+ danger: 'py-2 px-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600 focus:ring-red-500 dark:focus:ring-red-400 focus:ring-offset-gray-50 dark:focus:ring-offset-gray-900',
29
+ success: 'py-2 px-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-600 focus:ring-green-500 dark:focus:ring-green-400 focus:ring-offset-gray-50 dark:focus:ring-offset-gray-900',
30
+ outline: 'py-2 px-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-700 focus:ring-indigo-500 focus:ring-offset-gray-50 dark:focus:ring-offset-gray-900',
31
+ nav: 'w-full flex items-center px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 hover:scale-105 text-gray-700 dark:text-gray-300 dark:hover:text-white hover:shadow-lg',
32
+ custom: "",
33
+ link: 'text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 transition-colors duration-200',
34
+ warning: 'py-2 px-2 border border-transparent text-sm font-medium rounded-md text-white bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-500 dark:hover:bg-yellow-600 focus:ring-yellow-500 dark:focus:ring-yellow-400 focus:ring-offset-gray-50 dark:focus:ring-offset-gray-900',
35
+ toggle: 'px-2 py-2 rounded-lg font-medium transition-all duration-200 disabled:cursor-not-allowed border-2 focus:outline-none focus:ring-2 focus:ring-indigo-500'
36
+ };
37
+
38
+ let classes = `${baseClasses} ${variantClasses[variant]} ${className}`;
39
+
40
+ if (variant === 'nav' && isActive) {
41
+ classes += ' bg-indigo-600 dark:bg-indigo-500 hover:bg-indigo-700 dark:hover:bg-indigo-600 text-white shadow-lg scale-105';
42
+ }
43
+
44
+ if (variant === 'nav' && !isActive) {
45
+ classes += ' hover:bg-white dark:hover:bg-gray-700 hover:shadow-lg';
46
+ }
47
+
48
+ if (variant === 'toggle') {
49
+ if (isActive) {
50
+ classes += ' bg-indigo-600 text-white border-indigo-500 hover:bg-indigo-700';
51
+ } else {
52
+ classes += ' bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-300 dark:hover:bg-gray-600 hover:border-gray-400 dark:hover:border-gray-500';
53
+ }
54
+ }
55
+
56
+ return (
57
+ <button
58
+ className={classes}
59
+ disabled={disabled || isLoading}
60
+ {...props}
61
+ >
62
+ {isLoading ? (
63
+ <>
64
+ <AnimateSpin className="h-5 w-5 mr-2 inline-block text-current" />
65
+ {loadingText || 'Cargando...'}
66
+ </>
67
+ ) : (
68
+ children
69
+ )}
70
+ </button>
71
+ );
72
+ };
@@ -0,0 +1,40 @@
1
+ import { type FormHTMLAttributes, type FC, type FormEvent, type ReactNode } from 'react';
2
+
3
+ interface FormProps extends FormHTMLAttributes<HTMLFormElement> {
4
+ children: ReactNode;
5
+ onSubmit?: (e: FormEvent<HTMLFormElement>) => void;
6
+ variant?: 'default' | 'modal' | 'card' | 'inline' | 'compact';
7
+ }
8
+
9
+ export const Form: FC<FormProps> = ({ onSubmit, children, variant = 'default', className = '', ...props }) => {
10
+ const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
11
+ e.preventDefault();
12
+ if (onSubmit) {
13
+ onSubmit(e);
14
+ }
15
+ };
16
+
17
+ const getVariantClasses = () => {
18
+ switch (variant) {
19
+ case 'modal':
20
+ return 'flex-1 px-6 py-4 overflow-y-auto';
21
+ case 'card':
22
+ return 'p-6 space-y-6';
23
+ case 'inline':
24
+ return 'flex flex-wrap gap-4 items-end';
25
+ case 'compact':
26
+ return 'space-y-3';
27
+ case 'default':
28
+ default:
29
+ return 'space-y-4';
30
+ }
31
+ };
32
+
33
+ const combinedClassName = `${getVariantClasses()} ${className}`.trim();
34
+
35
+ return (
36
+ <form onSubmit={handleSubmit} className={combinedClassName} {...props}>
37
+ {children}
38
+ </form>
39
+ );
40
+ };
@@ -0,0 +1,91 @@
1
+ import { type InputHTMLAttributes, type FC, type ReactNode } from 'react';
2
+
3
+ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
4
+ label?: string | ReactNode;
5
+ error?: string;
6
+ helperText?: string;
7
+ }
8
+
9
+ export const Input: FC<InputProps> = ({
10
+ label,
11
+ error,
12
+ helperText,
13
+ className = '',
14
+ id,
15
+ type,
16
+ ...props
17
+ }) => {
18
+ const inputId = id || `input-${Math.random().toString(36).substring(2, 9)}`;
19
+
20
+ const baseClasses = 'appearance-none relative block w-full px-3 py-2 border placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white bg-white dark:bg-gray-800 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 focus:border-indigo-500 focus:z-10 sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200';
21
+
22
+ const errorClasses = error ? 'border-red-300 dark:border-red-600 focus:ring-red-500 dark:focus:ring-red-400 focus:border-red-500' : 'border-gray-300 dark:border-gray-600';
23
+
24
+ const classes = `${baseClasses} ${errorClasses} ${className}`;
25
+
26
+ if (type === 'checkbox') {
27
+ return (
28
+ <div className="space-y-1 w-full">
29
+ <div className="flex items-center space-x-2">
30
+ <input
31
+ id={inputId}
32
+ type="checkbox"
33
+ className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
34
+ {...props}
35
+ />
36
+ {label && typeof label === 'string' ? (
37
+ <label
38
+ htmlFor={inputId}
39
+ className="block text-sm font-medium text-gray-700 dark:text-gray-300"
40
+ >
41
+ {label}
42
+ </label>
43
+ ) : (
44
+ label
45
+ )}
46
+ </div>
47
+ {error && (
48
+ <p className="text-sm text-red-600 dark:text-red-400" role="alert">
49
+ {error}
50
+ </p>
51
+ )}
52
+ {helperText && !error && (
53
+ <p className="text-sm text-gray-500 dark:text-gray-400">
54
+ {helperText}
55
+ </p>
56
+ )}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ return (
62
+ <div className="space-y-1 w-full">
63
+ {label && typeof label === 'string' ? (
64
+ <label
65
+ htmlFor={inputId}
66
+ className="block text-sm font-medium text-gray-700 dark:text-gray-300"
67
+ >
68
+ {label}
69
+ </label>
70
+ ) : (
71
+ label
72
+ )}
73
+ <input
74
+ id={inputId}
75
+ className={classes}
76
+ type={type}
77
+ {...props}
78
+ />
79
+ {error && (
80
+ <p className="text-sm text-red-600 dark:text-red-400" role="alert">
81
+ {error}
82
+ </p>
83
+ )}
84
+ {helperText && !error && (
85
+ <p className="text-sm text-gray-500 dark:text-gray-400">
86
+ {helperText}
87
+ </p>
88
+ )}
89
+ </div>
90
+ );
91
+ };
@@ -0,0 +1,80 @@
1
+ import { Button } from './Button';
2
+ import { CloseIcon } from '../icons/icons';
3
+ import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
4
+
5
+ interface ModalProps {
6
+ onClose: () => void;
7
+ title: string;
8
+ children: React.ReactNode;
9
+ footer?: React.ReactNode;
10
+ maxWidth?: string;
11
+ showCloseButton?: boolean;
12
+ zIndex?: number;
13
+ }
14
+
15
+ export interface ModalRef {
16
+ handleClose: () => void;
17
+ }
18
+
19
+ export const Modal = forwardRef<ModalRef, ModalProps>(({
20
+ onClose,
21
+ title,
22
+ children,
23
+ footer,
24
+ maxWidth = 'max-w-2xl',
25
+ showCloseButton = true,
26
+ zIndex = 50
27
+ }, ref) => {
28
+ const [show, setShow] = useState(false);
29
+
30
+ useEffect(() => {
31
+ setShow(true);
32
+ }, []);
33
+
34
+ const handleClose = () => {
35
+ setShow(false);
36
+ setTimeout(() => {
37
+ onClose();
38
+ }, 300);
39
+ };
40
+
41
+ useImperativeHandle(ref, () => ({
42
+ handleClose
43
+ }));
44
+
45
+ return (
46
+ <dialog
47
+ open={show}
48
+ className={`fixed inset-0 w-full h-full flex items-center justify-center p-4 ${show && 'opacity-100'} transition-opacity opacity-0 duration-300 bg-gray-900/60 backdrop-blur-sm`}
49
+ style={{ zIndex: zIndex - 10 }}
50
+ >
51
+ <article
52
+ className={`relative bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-2xl w-full ${maxWidth} max-h-[90vh] flex flex-col overflow-hidden`}
53
+ style={{ zIndex }}
54
+ >
55
+ <header className="shrink-0 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4 flex items-center justify-between">
56
+ <h2 className="text-2xl font-bold text-gray-900 dark:text-white">{title}</h2>
57
+ {showCloseButton && (
58
+ <Button
59
+ variant='icon'
60
+ onClick={handleClose}
61
+ className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
62
+ >
63
+ <CloseIcon className="w-5 h-5" />
64
+ </Button>
65
+ )}
66
+ </header>
67
+ <div className="flex-1 overflow-y-auto p-6">
68
+ {children}
69
+ </div>
70
+ {footer && (
71
+ <footer className="shrink-0 bg-gray-50 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-6 py-4 flex justify-end gap-3">
72
+ {footer}
73
+ </footer>
74
+ )}
75
+ </article>
76
+ </dialog>
77
+ );
78
+ });
79
+
80
+ Modal.displayName = 'Modal';
@@ -0,0 +1,77 @@
1
+ import { type SelectHTMLAttributes, type FC, type ReactNode } from 'react';
2
+
3
+ interface Option {
4
+ value: string | number;
5
+ label: string;
6
+ disabled?: boolean;
7
+ selected?: boolean;
8
+ }
9
+
10
+ interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
11
+ options: Option[];
12
+ placeholder?: string;
13
+ variant?: 'default' | 'small';
14
+ error?: boolean;
15
+ helperText?: string;
16
+ label?: string | ReactNode;
17
+ }
18
+
19
+ export const Select: FC<SelectProps> = ({
20
+ options,
21
+ placeholder,
22
+ variant = 'default',
23
+ error = false,
24
+ helperText,
25
+ label,
26
+ className = '',
27
+ id,
28
+ ...props
29
+ }) => {
30
+ const selectId = id || `select-${Math.random().toString(36).substring(2, 9)}`;
31
+
32
+ const getVariantClasses = () => {
33
+ const baseClasses = 'w-full bg-white dark:bg-gray-700 border rounded-lg text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed transition-colors';
34
+
35
+ if (variant === 'small') {
36
+ return `${baseClasses} px-2.5 py-1.5 text-sm border-gray-300 dark:border-gray-600`;
37
+ }
38
+
39
+ return `${baseClasses} px-3 py-2 border-gray-300 dark:border-gray-600 ${error ? 'border-red-300 dark:border-red-600 focus:ring-red-500' : ''}`;
40
+ };
41
+
42
+ const combinedClassName = `${getVariantClasses()} ${className}`.trim();
43
+
44
+ return (
45
+ <div className="space-y-1 w-full">
46
+ {label && typeof label === 'string' ? (
47
+ <label htmlFor={selectId} className="block text-xs font-normal text-gray-700 dark:text-gray-300">
48
+ {label}
49
+ </label>
50
+ ) : (
51
+ label
52
+ )}
53
+ <select id={selectId} className={combinedClassName} {...props}>
54
+ {placeholder && placeholder.trim() && (
55
+ <option value="" disabled>
56
+ {placeholder}
57
+ </option>
58
+ )}
59
+ {options.map((option) => (
60
+ <option
61
+ key={option.value}
62
+ value={option.value}
63
+ disabled={option.disabled}
64
+ selected={option.selected}
65
+ >
66
+ {option.label}
67
+ </option>
68
+ ))}
69
+ </select>
70
+ {helperText && (
71
+ <p className={`mt-1 text-sm ${error ? 'text-red-600 dark:text-red-400' : 'text-gray-600 dark:text-gray-400'}`}>
72
+ {helperText}
73
+ </p>
74
+ )}
75
+ </div>
76
+ );
77
+ };
@@ -0,0 +1,62 @@
1
+ import { type ReactNode } from 'react';
2
+
3
+ interface TableProps {
4
+ headers: ReactNode[];
5
+ rows: ReactNode[][];
6
+ variant?: 'default' | 'custom';
7
+ className?: string;
8
+ thClassName?: string;
9
+ tdClassName?: string;
10
+ }
11
+
12
+ export function Table({
13
+ headers,
14
+ rows,
15
+ variant = 'default',
16
+ className = '',
17
+ thClassName = '',
18
+ tdClassName = ''
19
+ }: TableProps) {
20
+ const baseTableClass = variant === 'default'
21
+ ? 'w-full table-auto border-collapse border border-gray-300 dark:border-gray-600 min-w-full'
22
+ : '';
23
+
24
+ const baseThClass = variant === 'default'
25
+ ? 'border border-gray-300 dark:border-gray-600 px-4 py-2 text-left text-gray-900 dark:text-white'
26
+ : '';
27
+
28
+ const baseTdClass = variant === 'default'
29
+ ? 'border border-gray-300 dark:border-gray-600 px-4 py-2 text-gray-900 dark:text-white'
30
+ : '';
31
+
32
+ const tableClass = `${baseTableClass} ${className}`.trim();
33
+ const thClass = `${baseThClass} ${thClassName}`.trim();
34
+ const tdClass = `${baseTdClass} ${tdClassName}`.trim();
35
+
36
+ return (
37
+ <div className="overflow-x-auto w-full">
38
+ <table className={tableClass}>
39
+ <thead>
40
+ <tr className={variant === 'default' ? 'bg-gray-100 dark:bg-gray-700' : ''}>
41
+ {headers.map((header, index) => (
42
+ <th key={index} className={thClass}>
43
+ {header}
44
+ </th>
45
+ ))}
46
+ </tr>
47
+ </thead>
48
+ <tbody>
49
+ {rows.map((row, rowIndex) => (
50
+ <tr key={rowIndex} className={variant === 'default' ? 'hover:bg-gray-50 dark:hover:bg-gray-600' : ''}>
51
+ {row.map((cell, cellIndex) => (
52
+ <td key={cellIndex} className={tdClass}>
53
+ {cell}
54
+ </td>
55
+ ))}
56
+ </tr>
57
+ ))}
58
+ </tbody>
59
+ </table>
60
+ </div>
61
+ );
62
+ }
@@ -0,0 +1,71 @@
1
+ import { type TextareaHTMLAttributes, type FC, type ReactNode } from 'react';
2
+
3
+ interface TextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
4
+ label?: string | ReactNode;
5
+ error?: string;
6
+ helperText?: string;
7
+ variant?: 'default' | 'outline' | 'filled' | 'minimal';
8
+ size?: 'small' | 'medium' | 'large';
9
+ }
10
+
11
+ export const TextArea: FC<TextAreaProps> = ({
12
+ label,
13
+ error,
14
+ helperText,
15
+ variant = 'default',
16
+ size = 'medium',
17
+ className = '',
18
+ id,
19
+ ...props
20
+ }) => {
21
+ const textAreaId = id || `textarea-${Math.random().toString(36).substring(2, 9)}`;
22
+
23
+ const sizeClasses = {
24
+ small: 'px-2 py-1 text-xs',
25
+ medium: 'px-3 py-2 text-sm',
26
+ large: 'px-4 py-3 text-base'
27
+ };
28
+
29
+ const variantClasses = {
30
+ default: 'border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800',
31
+ outline: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent',
32
+ filled: 'border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-700',
33
+ minimal: 'border-0 bg-transparent focus:ring-0 focus:border-0'
34
+ };
35
+
36
+ const baseClasses = 'appearance-none relative block w-full placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 focus:border-indigo-500 focus:z-10 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200 resize-vertical';
37
+
38
+ const errorClasses = error ? 'border-red-300 dark:border-red-600 focus:ring-red-500 dark:focus:ring-red-400 focus:border-red-500' : '';
39
+
40
+ const classes = `${baseClasses} ${sizeClasses[size]} ${variantClasses[variant]} ${errorClasses} ${className}`;
41
+
42
+ return (
43
+ <div className="space-y-1 w-full">
44
+ {label && typeof label === 'string' ? (
45
+ <label
46
+ htmlFor={textAreaId}
47
+ className="block text-sm font-medium text-gray-700 dark:text-gray-300"
48
+ >
49
+ {label}
50
+ </label>
51
+ ) : (
52
+ label
53
+ )}
54
+ <textarea
55
+ id={textAreaId}
56
+ className={classes}
57
+ {...props}
58
+ />
59
+ {error && (
60
+ <p className="text-sm text-red-600 dark:text-red-400" role="alert">
61
+ {error}
62
+ </p>
63
+ )}
64
+ {helperText && !error && (
65
+ <p className="text-sm text-gray-500 dark:text-gray-400">
66
+ {helperText}
67
+ </p>
68
+ )}
69
+ </div>
70
+ );
71
+ };
@@ -0,0 +1,7 @@
1
+ export { Button } from './Button';
2
+ export { Input } from './Input';
3
+ export { TextArea } from './TextArea';
4
+ export { Form } from './Form';
5
+ export { Select } from './Select';
6
+ export { Table } from './Table';
7
+ export { Modal, type ModalRef } from './Modal';