neogestify-ui-components 2.2.2 → 2.3.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +803 -349
  3. package/dist/components/ElementLibraryBuilder/index.js +299 -120
  4. package/dist/components/ElementLibraryBuilder/index.js.map +1 -1
  5. package/dist/components/ElementLibraryBuilder/index.mjs +300 -121
  6. package/dist/components/ElementLibraryBuilder/index.mjs.map +1 -1
  7. package/dist/components/VenueMapEditor/index.js.map +1 -1
  8. package/dist/components/VenueMapEditor/index.mjs.map +1 -1
  9. package/dist/components/alerts/index.js +108 -51
  10. package/dist/components/alerts/index.js.map +1 -1
  11. package/dist/components/alerts/index.mjs +109 -52
  12. package/dist/components/alerts/index.mjs.map +1 -1
  13. package/dist/components/html/index.d.mts +101 -22
  14. package/dist/components/html/index.d.ts +101 -22
  15. package/dist/components/html/index.js +506 -166
  16. package/dist/components/html/index.js.map +1 -1
  17. package/dist/components/html/index.mjs +507 -167
  18. package/dist/components/html/index.mjs.map +1 -1
  19. package/dist/components/icons/index.d.mts +5 -1
  20. package/dist/components/icons/index.d.ts +5 -1
  21. package/dist/components/icons/index.js +16 -0
  22. package/dist/components/icons/index.js.map +1 -1
  23. package/dist/components/icons/index.mjs +13 -1
  24. package/dist/components/icons/index.mjs.map +1 -1
  25. package/dist/context/theme/index.js +59 -37
  26. package/dist/context/theme/index.js.map +1 -1
  27. package/dist/context/theme/index.mjs +59 -37
  28. package/dist/context/theme/index.mjs.map +1 -1
  29. package/dist/index.d.mts +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.js +510 -166
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.mjs +508 -168
  34. package/dist/index.mjs.map +1 -1
  35. package/package.json +1 -1
  36. package/src/components/html/Button.tsx +84 -38
  37. package/src/components/html/Form.tsx +33 -13
  38. package/src/components/html/Input.tsx +110 -31
  39. package/src/components/html/Loading.tsx +58 -33
  40. package/src/components/html/Modal.tsx +67 -20
  41. package/src/components/html/Select.tsx +92 -39
  42. package/src/components/html/Table.tsx +274 -50
  43. package/src/components/html/TextArea.tsx +81 -31
  44. package/src/components/icons/icons.tsx +32 -0
@@ -1,62 +1,109 @@
1
1
  import { Button } from './Button';
2
2
  import { CloseIcon } from '../icons/icons';
3
- import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
3
+ import React, { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react';
4
+
5
+ type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
6
+ type ModalVariant = 'default' | 'danger' | 'success' | 'warning';
4
7
 
5
8
  interface ModalProps {
6
9
  onClose: () => void;
7
- title: string;
10
+ title: React.ReactNode;
8
11
  children: React.ReactNode;
9
12
  footer?: React.ReactNode;
13
+ /** @deprecated Use size instead */
10
14
  maxWidth?: string;
15
+ size?: ModalSize;
11
16
  showCloseButton?: boolean;
12
17
  zIndex?: number;
18
+ closeOnBackdrop?: boolean;
19
+ closeOnEsc?: boolean;
20
+ variant?: ModalVariant;
13
21
  }
14
22
 
15
23
  export interface ModalRef {
16
24
  handleClose: () => void;
17
25
  }
18
26
 
27
+ const SIZE_CLASS: Record<ModalSize, string> = {
28
+ sm: 'max-w-sm',
29
+ md: 'max-w-md',
30
+ lg: 'max-w-2xl',
31
+ xl: 'max-w-4xl',
32
+ full: 'max-w-[95vw] w-[95vw]',
33
+ };
34
+
35
+ const VARIANT_HEADER: Record<ModalVariant, string> = {
36
+ default: 'bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700',
37
+ danger: 'bg-red-50 dark:bg-red-900/30 border-b border-red-200 dark:border-red-800',
38
+ success: 'bg-green-50 dark:bg-green-900/30 border-b border-green-200 dark:border-green-800',
39
+ warning: 'bg-yellow-50 dark:bg-yellow-900/30 border-b border-yellow-200 dark:border-yellow-800',
40
+ };
41
+
42
+ const VARIANT_TITLE: Record<ModalVariant, string> = {
43
+ default: 'text-gray-900 dark:text-white',
44
+ danger: 'text-red-700 dark:text-red-300',
45
+ success: 'text-green-700 dark:text-green-300',
46
+ warning: 'text-yellow-700 dark:text-yellow-300',
47
+ };
48
+
19
49
  export const Modal = forwardRef<ModalRef, ModalProps>(({
20
50
  onClose,
21
51
  title,
22
52
  children,
23
53
  footer,
24
- maxWidth = 'max-w-2xl',
54
+ maxWidth,
55
+ size,
25
56
  showCloseButton = true,
26
- zIndex = 50
57
+ zIndex = 50,
58
+ closeOnBackdrop = false,
59
+ closeOnEsc = false,
60
+ variant = 'default',
27
61
  }, ref) => {
28
62
  const [show, setShow] = useState(false);
29
-
30
- useEffect(() => {
31
- setShow(true);
32
- }, []);
63
+ const handleCloseRef = useRef<() => void>(() => {});
33
64
 
34
65
  const handleClose = () => {
35
66
  setShow(false);
36
- setTimeout(() => {
37
- onClose();
38
- }, 300);
67
+ setTimeout(() => onClose(), 300);
39
68
  };
40
69
 
41
- useImperativeHandle(ref, () => ({
42
- handleClose
43
- }));
70
+ handleCloseRef.current = handleClose;
71
+
72
+ useEffect(() => { setShow(true); }, []);
73
+
74
+ useEffect(() => {
75
+ if (!closeOnEsc) return;
76
+ const handler = (e: KeyboardEvent) => {
77
+ if (e.key === 'Escape') handleCloseRef.current();
78
+ };
79
+ document.addEventListener('keydown', handler);
80
+ return () => document.removeEventListener('keydown', handler);
81
+ }, [closeOnEsc]);
82
+
83
+ useImperativeHandle(ref, () => ({ handleClose }));
84
+
85
+ const widthCls = size ? SIZE_CLASS[size] : (maxWidth ?? 'max-w-2xl');
86
+
87
+ const handleBackdropClick = (e: React.MouseEvent) => {
88
+ if (closeOnBackdrop && e.target === e.currentTarget) handleClose();
89
+ };
44
90
 
45
91
  return (
46
92
  <dialog
47
93
  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`}
94
+ className={`fixed inset-0 w-full h-full flex items-center justify-center p-4 transition-opacity duration-300 bg-gray-900/60 backdrop-blur-sm ${show ? 'opacity-100' : 'opacity-0'}`}
49
95
  style={{ zIndex: zIndex - 10 }}
96
+ onClick={handleBackdropClick}
50
97
  >
51
98
  <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`}
99
+ className={`relative bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-2xl w-full ${widthCls} max-h-[90vh] flex flex-col overflow-hidden`}
53
100
  style={{ zIndex }}
54
101
  >
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>
102
+ <header className={`shrink-0 px-6 py-4 flex items-center justify-between ${VARIANT_HEADER[variant]}`}>
103
+ <h2 className={`text-2xl font-bold ${VARIANT_TITLE[variant]}`}>{title}</h2>
57
104
  {showCloseButton && (
58
105
  <Button
59
- variant='icon'
106
+ variant="icon"
60
107
  onClick={handleClose}
61
108
  className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
62
109
  >
@@ -77,4 +124,4 @@ export const Modal = forwardRef<ModalRef, ModalProps>(({
77
124
  );
78
125
  });
79
126
 
80
- Modal.displayName = 'Modal';
127
+ Modal.displayName = 'Modal';
@@ -1,4 +1,8 @@
1
1
  import { type SelectHTMLAttributes, type FC, type ReactNode } from 'react';
2
+ import { ChevronDownIcon } from '../icons/icons';
3
+
4
+ type SelectVariant = 'default' | 'outline' | 'filled' | 'minimal' | 'custom' | 'small';
5
+ type SelectSize = 'sm' | 'md' | 'lg';
2
6
 
3
7
  interface Option {
4
8
  value: string | number;
@@ -10,73 +14,122 @@ interface Option {
10
14
  interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
11
15
  options: Option[];
12
16
  placeholder?: string;
13
- variant?: 'default' | 'small';
14
- error?: boolean;
17
+ variant?: SelectVariant;
18
+ size?: SelectSize;
19
+ error?: string | boolean;
15
20
  helperText?: string;
16
21
  label?: string | ReactNode;
22
+ icon?: ReactNode;
17
23
  }
18
24
 
25
+ const SIZE_CLASSES: Record<SelectSize, string> = {
26
+ sm: 'py-1.5 text-xs',
27
+ md: 'py-2 text-sm',
28
+ lg: 'py-2.5 text-base',
29
+ };
30
+
31
+ const VARIANT_CLASSES: Record<Exclude<SelectVariant, 'small'>, string> = {
32
+ default: 'border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800',
33
+ outline: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent',
34
+ filled: 'border border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-700',
35
+ minimal: 'border-0 border-b border-gray-300 dark:border-gray-600 bg-transparent rounded-none focus:ring-0',
36
+ custom: '',
37
+ };
38
+
19
39
  export const Select: FC<SelectProps> = ({
20
40
  options,
21
41
  placeholder,
22
42
  variant = 'default',
43
+ size = 'md',
23
44
  error = false,
24
45
  helperText,
25
46
  label,
47
+ icon,
26
48
  className = '',
27
49
  id,
28
50
  ...props
29
51
  }) => {
30
52
  const selectId = id || `select-${Math.random().toString(36).substring(2, 9)}`;
31
53
 
32
- const getVariantClasses = () => {
33
- 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';
54
+ // backward compat: variant='small' size='sm'
55
+ const effectiveSize: SelectSize = variant === 'small' ? 'sm' : size;
56
+ const effectiveVariant: Exclude<SelectVariant, 'small'> = variant === 'small' ? 'default' : variant;
34
57
 
35
- if (variant === 'small') {
36
- return `${baseClasses.replace('px-3 py-2', 'px-2.5 py-1.5 text-sm')} border-gray-300 dark:border-gray-600`;
37
- }
58
+ const hasError = Boolean(error);
59
+ const errorMsg = typeof error === 'string' ? error : '';
38
60
 
39
- return `${baseClasses} border-gray-300 dark:border-gray-600 ${error ? 'border-red-300 dark:border-red-600 focus:ring-red-500 dark:focus:ring-red-400 focus:border-red-500' : ''}`;
40
- };
61
+ // Compute defaultValue from options[].selected when no controlled value provided
62
+ const computedDefaultValue =
63
+ props.value === undefined && props.defaultValue === undefined
64
+ ? options.find((o) => o.selected)?.value?.toString()
65
+ : undefined;
41
66
 
42
- const getOptionClasses = (option: Option) => {
43
- return `bg-white dark:bg-gray-800 text-gray-900 dark:text-white py-2 ${option.disabled ? 'opacity-50 cursor-not-allowed' : ''}`;
44
- };
67
+ const baseCls =
68
+ 'appearance-none relative block w-full pl-3 pr-9 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';
45
69
 
46
- const combinedClassName = `${getVariantClasses()} ${className}`.trim();
70
+ const errorCls = hasError
71
+ ? 'border-red-300 dark:border-red-600 focus:ring-red-500 dark:focus:ring-red-400 focus:border-red-500'
72
+ : '';
73
+
74
+ const selectCls = [
75
+ baseCls,
76
+ SIZE_CLASSES[effectiveSize],
77
+ VARIANT_CLASSES[effectiveVariant],
78
+ errorCls,
79
+ icon ? 'pl-9' : '',
80
+ className,
81
+ ].filter(Boolean).join(' ');
47
82
 
48
83
  return (
49
84
  <div className="space-y-1 w-full">
50
- {label && typeof label === 'string' ? (
51
- <label htmlFor={selectId} className="block text-sm font-medium text-gray-700 dark:text-gray-300">
52
- {label}
53
- </label>
54
- ) : (
55
- label
85
+ {label && (
86
+ typeof label === 'string' ? (
87
+ <label htmlFor={selectId} className="block text-sm font-medium text-gray-700 dark:text-gray-300">
88
+ {label}
89
+ {props.required && <span className="ml-1 text-red-500" aria-hidden="true">*</span>}
90
+ </label>
91
+ ) : label
56
92
  )}
57
- <select id={selectId} className={combinedClassName} {...props}>
58
- {placeholder && placeholder.trim() && (
59
- <option value="" disabled selected className="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 py-2">
60
- {placeholder}
61
- </option>
93
+ <div className="relative">
94
+ {icon && (
95
+ <div className="pointer-events-none absolute inset-y-0 left-0 z-10 flex items-center pl-3 text-gray-400 dark:text-gray-500">
96
+ {icon}
97
+ </div>
62
98
  )}
63
- {options.map((option) => (
64
- <option
65
- key={option.value}
66
- value={option.value}
67
- disabled={option.disabled}
68
- selected={option.selected}
69
- className={getOptionClasses(option)}
70
- >
71
- {option.label}
72
- </option>
73
- ))}
74
- </select>
75
- {helperText && (
76
- <p className={`text-sm ${error ? 'text-red-600 dark:text-red-400' : 'text-gray-500 dark:text-gray-400'}`}>
99
+ <select
100
+ id={selectId}
101
+ className={selectCls}
102
+ defaultValue={computedDefaultValue}
103
+ {...props}
104
+ >
105
+ {placeholder && (
106
+ <option value="" disabled className="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400">
107
+ {placeholder}
108
+ </option>
109
+ )}
110
+ {options.map((option) => (
111
+ <option
112
+ key={option.value}
113
+ value={option.value}
114
+ disabled={option.disabled}
115
+ className={`bg-white dark:bg-gray-800 text-gray-900 dark:text-white${option.disabled ? ' opacity-50' : ''}`}
116
+ >
117
+ {option.label}
118
+ </option>
119
+ ))}
120
+ </select>
121
+ <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2.5 text-gray-400 dark:text-gray-500">
122
+ <ChevronDownIcon className="w-4 h-4" />
123
+ </div>
124
+ </div>
125
+ {errorMsg && (
126
+ <p className="text-sm text-red-600 dark:text-red-400" role="alert">{errorMsg}</p>
127
+ )}
128
+ {helperText && !errorMsg && (
129
+ <p className={`text-sm ${hasError ? 'text-red-600 dark:text-red-400' : 'text-gray-500 dark:text-gray-400'}`}>
77
130
  {helperText}
78
131
  </p>
79
132
  )}
80
133
  </div>
81
134
  );
82
- };
135
+ };