@capyx/components-library 0.0.1

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,144 @@
1
+ import React, { type ChangeEvent, type FC } from 'react';
2
+ import { Form } from 'react-bootstrap';
3
+ import { Controller, useFormContext } from 'react-hook-form';
4
+
5
+ /**
6
+ * Props for the SelectInput component
7
+ */
8
+ export type SelectInputProps = {
9
+ /** The name of the select input field */
10
+ name: string;
11
+ /** Label text displayed for the select */
12
+ label?: string;
13
+ /** Array of option strings to display in the dropdown */
14
+ options: string[];
15
+ /** Placeholder text shown when no option is selected (default: "Select...") */
16
+ helpText?: string;
17
+ /** Size of the select control */
18
+ controlSize?: 'sm' | 'lg';
19
+ /** Current selected value (standalone mode) */
20
+ value?: string;
21
+ /** Callback function called when the selection changes */
22
+ onChange?: (value: string) => void;
23
+ /** Whether the select is disabled */
24
+ disabled?: boolean;
25
+ /** Whether the select is required */
26
+ required?: boolean;
27
+ /** Array of option values to display in bold */
28
+ highlightValues?: string[];
29
+ };
30
+
31
+ /**
32
+ * SelectInput - A dropdown select component with react-hook-form integration
33
+ *
34
+ * Provides a select dropdown that works both standalone and with react-hook-form.
35
+ * Supports option highlighting, custom placeholder text, and automatic validation.
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * // With react-hook-form
40
+ * <SelectInput
41
+ * name="country"
42
+ * label="Country"
43
+ * options={["USA", "Canada", "UK"]}
44
+ * required
45
+ * />
46
+ *
47
+ * // Standalone mode
48
+ * <SelectInput
49
+ * name="role"
50
+ * options={["Admin", "User", "Guest"]}
51
+ * value={selectedRole}
52
+ * onChange={setSelectedRole}
53
+ * highlightValues={["Admin"]}
54
+ * />
55
+ * ```
56
+ */
57
+ export const SelectInput: FC<SelectInputProps> = ({
58
+ name,
59
+ label,
60
+ options = [],
61
+ helpText = 'Select...',
62
+ controlSize,
63
+ value,
64
+ onChange,
65
+ disabled = false,
66
+ required = false,
67
+ highlightValues = [],
68
+ }) => {
69
+ const formContext = useFormContext();
70
+
71
+ const getFieldError = (fieldName: string) => {
72
+ try {
73
+ const error = formContext?.formState?.errors?.[fieldName];
74
+ return error?.message as string | undefined;
75
+ } catch {
76
+ return undefined;
77
+ }
78
+ };
79
+
80
+ const errorMessage = getFieldError(name);
81
+ const isInvalid = !!errorMessage;
82
+
83
+ const handleSelectChange = (event: ChangeEvent<HTMLSelectElement>) => {
84
+ onChange?.(event.currentTarget.value);
85
+ };
86
+
87
+ const renderOptions = () => (
88
+ <>
89
+ <option value="">{helpText.trim()}</option>
90
+ {options.map((option: string) => {
91
+ const style = highlightValues.includes(option)
92
+ ? { fontWeight: 'bold' as const }
93
+ : {};
94
+ const key = option.replace(/ /g, '');
95
+ return (
96
+ <option style={style} key={key} value={key}>
97
+ {option.trim()}
98
+ </option>
99
+ );
100
+ })}
101
+ </>
102
+ );
103
+
104
+ // Integrated with react-hook-form
105
+ if (formContext) {
106
+ return (
107
+ <Controller
108
+ name={name}
109
+ control={formContext.control}
110
+ rules={{
111
+ required: required ? `${label || 'This field'} is required` : false,
112
+ }}
113
+ render={({ field }) => (
114
+ <Form.Select
115
+ {...field}
116
+ onChange={(e) => {
117
+ field.onChange(e);
118
+ onChange?.(e.target.value);
119
+ }}
120
+ required={required}
121
+ size={controlSize}
122
+ disabled={disabled}
123
+ isInvalid={isInvalid}
124
+ >
125
+ {renderOptions()}
126
+ </Form.Select>
127
+ )}
128
+ />
129
+ );
130
+ }
131
+
132
+ // Standalone mode
133
+ return (
134
+ <Form.Select
135
+ required={required}
136
+ size={controlSize}
137
+ value={value || ''}
138
+ onChange={handleSelectChange}
139
+ disabled={disabled}
140
+ >
141
+ {renderOptions()}
142
+ </Form.Select>
143
+ );
144
+ };
@@ -0,0 +1,116 @@
1
+ import React, { type FC } from 'react';
2
+ import { Form } from 'react-bootstrap';
3
+ import { Controller, useFormContext } from 'react-hook-form';
4
+
5
+ /**
6
+ * Props for the SwitchInput component
7
+ */
8
+ export type SwitchInputProps = {
9
+ /** The name of the switch input field */
10
+ name: string;
11
+ /** Label text displayed next to the switch */
12
+ label?: string;
13
+ /** Whether the switch is required */
14
+ required?: boolean;
15
+ /** Current on/off state (standalone mode) */
16
+ value?: boolean;
17
+ /** Callback function called when the switch state changes */
18
+ onChange?: (checked: boolean) => void;
19
+ /** Whether the switch is disabled */
20
+ disabled?: boolean;
21
+ /** Custom HTML id for the switch element */
22
+ id?: string;
23
+ };
24
+
25
+ /**
26
+ * SwitchInput - A toggle switch component with react-hook-form integration
27
+ *
28
+ * Provides a toggle switch input that works both standalone and with react-hook-form.
29
+ * Automatically integrates with FormProvider when available, providing validation
30
+ * and error handling.
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * // With react-hook-form
35
+ * <SwitchInput
36
+ * name="notifications"
37
+ * label="Enable notifications"
38
+ * />
39
+ *
40
+ * // Standalone mode
41
+ * <SwitchInput
42
+ * name="darkMode"
43
+ * label="Dark mode"
44
+ * value={isDarkMode}
45
+ * onChange={setIsDarkMode}
46
+ * />
47
+ * ```
48
+ */
49
+ export const SwitchInput: FC<SwitchInputProps> = ({
50
+ name,
51
+ label,
52
+ required = false,
53
+ value,
54
+ onChange,
55
+ disabled = false,
56
+ id,
57
+ }) => {
58
+ const formContext = useFormContext();
59
+
60
+ const getFieldError = (fieldName: string) => {
61
+ try {
62
+ const error = formContext?.formState?.errors?.[fieldName];
63
+ return error?.message as string | undefined;
64
+ } catch {
65
+ return undefined;
66
+ }
67
+ };
68
+
69
+ const errorMessage = getFieldError(name);
70
+ const inputId = id || `switch-input-${name}`;
71
+
72
+ // Integrated with react-hook-form
73
+ if (formContext) {
74
+ return (
75
+ <Controller
76
+ name={name}
77
+ control={formContext.control}
78
+ rules={{
79
+ required: required ? `${label || 'This field'} is required` : false,
80
+ }}
81
+ render={({ field }) => (
82
+ <Form.Check
83
+ {...field}
84
+ id={inputId}
85
+ type="switch"
86
+ label={label}
87
+ checked={field.value ?? false}
88
+ onChange={(e) => {
89
+ const checked = e.target.checked;
90
+ field.onChange(checked);
91
+ onChange?.(checked);
92
+ }}
93
+ disabled={disabled}
94
+ required={required}
95
+ isInvalid={!!errorMessage}
96
+ feedback={errorMessage}
97
+ feedbackType="invalid"
98
+ />
99
+ )}
100
+ />
101
+ );
102
+ }
103
+
104
+ // Standalone mode
105
+ return (
106
+ <Form.Check
107
+ id={inputId}
108
+ type="switch"
109
+ label={label}
110
+ checked={value ?? false}
111
+ onChange={(e) => onChange?.(e.target.checked)}
112
+ disabled={disabled}
113
+ required={required}
114
+ />
115
+ );
116
+ };
@@ -0,0 +1,118 @@
1
+ import { Autocomplete, Chip, TextField } from '@mui/material';
2
+ import React, { type FC } from 'react';
3
+
4
+ /**
5
+ * Props for the TagsInput component
6
+ */
7
+ export type TagsInputProps = {
8
+ /** Array of current tag values */
9
+ value: string[];
10
+ /** Callback function called when tags change */
11
+ onChange: (value: string[]) => void;
12
+ /** Placeholder text shown when no tags are entered (default: "Add tags...") */
13
+ placeholder?: string;
14
+ /** Whether the input is disabled */
15
+ disabled?: boolean;
16
+ /** Name attribute for the input field */
17
+ name?: string;
18
+ };
19
+
20
+ /**
21
+ * TagsInput - A multi-tag input component using Material-UI Autocomplete
22
+ *
23
+ * Provides a tag input interface where users can add/remove multiple tags.
24
+ * Automatically trims whitespace and filters empty values. Tags are displayed
25
+ * as chips with delete functionality.
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * <TagsInput
30
+ * name="skills"
31
+ * value={tags}
32
+ * onChange={setTags}
33
+ * placeholder="Add skills..."
34
+ * />
35
+ * ```
36
+ */
37
+ export const TagsInput: FC<TagsInputProps> = ({
38
+ value = [],
39
+ onChange,
40
+ placeholder = 'Add tags...',
41
+ disabled = false,
42
+ name,
43
+ }) => {
44
+ return (
45
+ <Autocomplete
46
+ multiple
47
+ freeSolo
48
+ options={[]}
49
+ value={value}
50
+ onChange={(_event, newValue) => {
51
+ // Filter out empty strings and trim whitespace
52
+ const cleanedValues = newValue
53
+ .map((v) => (typeof v === 'string' ? v.trim() : v))
54
+ .filter((v) => v.length > 0);
55
+ onChange(cleanedValues);
56
+ }}
57
+ disabled={disabled}
58
+ renderValue={(tagValue, getTagProps) =>
59
+ tagValue.map((option, index) => (
60
+ <Chip
61
+ {...getTagProps({ index })}
62
+ key={option}
63
+ label={option}
64
+ sx={{
65
+ backgroundColor: '#212529',
66
+ color: '#ffffff',
67
+ '& .MuiChip-deleteIcon': {
68
+ color: 'rgba(255, 255, 255, 0.7)',
69
+ userSelect: 'none',
70
+ WebkitUserSelect: 'none',
71
+ MozUserSelect: 'none',
72
+ msUserSelect: 'none',
73
+ pointerEvents: 'auto',
74
+ '&::selection': {
75
+ backgroundColor: 'transparent',
76
+ color: 'transparent',
77
+ },
78
+ '& svg': {
79
+ userSelect: 'none',
80
+ WebkitUserSelect: 'none',
81
+ pointerEvents: 'none',
82
+ },
83
+ '&:hover': {
84
+ color: '#dc3545',
85
+ },
86
+ },
87
+ }}
88
+ />
89
+ ))
90
+ }
91
+ renderInput={(params) => (
92
+ <TextField
93
+ {...params}
94
+ name={name}
95
+ placeholder={value.length === 0 ? placeholder : undefined}
96
+ variant="outlined"
97
+ size="small"
98
+ sx={{
99
+ '& .MuiOutlinedInput-root': {
100
+ padding: '4px',
101
+ minHeight: '38px',
102
+ '& fieldset': {
103
+ borderColor: '#ced4da',
104
+ },
105
+ '&:hover fieldset': {
106
+ borderColor: '#86b7fe',
107
+ },
108
+ '&.Mui-focused fieldset': {
109
+ borderColor: '#86b7fe',
110
+ borderWidth: '1px',
111
+ },
112
+ },
113
+ }}
114
+ />
115
+ )}
116
+ />
117
+ );
118
+ };
@@ -0,0 +1,211 @@
1
+ import debounce from 'lodash.debounce';
2
+ import React, {
3
+ type ChangeEvent,
4
+ type FC,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
10
+ import { Form } from 'react-bootstrap';
11
+ import { Controller, useFormContext } from 'react-hook-form';
12
+
13
+ /**
14
+ * Props for the TextAreaInput component
15
+ */
16
+ export type TextAreaInputProps = {
17
+ /** The name of the textarea field */
18
+ name: string;
19
+ /** Label text displayed for the textarea */
20
+ label?: string;
21
+ /** Whether the field is required */
22
+ required?: boolean;
23
+ /** Maximum number of characters allowed */
24
+ maxLength?: number;
25
+ /** Size variant of the textarea control */
26
+ controlSize?: 'sm' | 'lg';
27
+ /** Placeholder text shown when textarea is empty */
28
+ placeholder?: string;
29
+ /** Controlled value of the textarea */
30
+ value?: string;
31
+ /** Callback fired when the value changes */
32
+ onChange?: (value: string) => void;
33
+ /** Whether the textarea is disabled */
34
+ disabled?: boolean;
35
+ /** Whether the textarea is read-only */
36
+ isReadOnly?: boolean;
37
+ /** Whether to render as plain text */
38
+ isPlainText?: boolean;
39
+ /** Debounce delay in milliseconds for value changes */
40
+ debounceMs?: number;
41
+ };
42
+
43
+ const MIN_TEXTAREA_HEIGHT = 32;
44
+
45
+ /**
46
+ * A flexible textarea input component with automatic height adjustment,
47
+ * react-hook-form integration, and optional debouncing.
48
+ *
49
+ * Features:
50
+ * - Auto-expands height based on content
51
+ * - Seamless integration with react-hook-form for validation
52
+ * - Debounced onChange callback to reduce update frequency
53
+ * - Built-in validation rules (required, maxLength)
54
+ * - Works in both controlled and standalone modes
55
+ *
56
+ * @example
57
+ * // Basic usage with react-hook-form
58
+ * <TextAreaInput
59
+ * name="description"
60
+ * label="Description"
61
+ * required
62
+ * maxLength={500}
63
+ * placeholder="Enter description..."
64
+ * />
65
+ *
66
+ * @example
67
+ * // With custom onChange and debouncing
68
+ * <TextAreaInput
69
+ * name="notes"
70
+ * label="Notes"
71
+ * debounceMs={300}
72
+ * onChange={(value) => console.log(value)}
73
+ * />
74
+ *
75
+ * @example
76
+ * // Standalone mode without form context
77
+ * <TextAreaInput
78
+ * name="comment"
79
+ * value={commentText}
80
+ * onChange={setCommentText}
81
+ * placeholder="Add your comment..."
82
+ * />
83
+ */
84
+ export const TextAreaInput: FC<TextAreaInputProps> = ({
85
+ name,
86
+ label,
87
+ required = false,
88
+ maxLength,
89
+ controlSize,
90
+ placeholder,
91
+ value,
92
+ onChange,
93
+ disabled = false,
94
+ isReadOnly = false,
95
+ isPlainText = false,
96
+ debounceMs,
97
+ }) => {
98
+ const formContext = useFormContext();
99
+
100
+ // Create ref for debounced onChange to clean up on unmount
101
+ const debouncedOnChangeRef = useRef<ReturnType<typeof debounce> | null>(null);
102
+
103
+ // Create debounced version of onChange if debounceMs is provided
104
+ useEffect(() => {
105
+ if (debounceMs && onChange) {
106
+ debouncedOnChangeRef.current = debounce(onChange, debounceMs);
107
+ }
108
+
109
+ // Cleanup on unmount
110
+ return () => {
111
+ debouncedOnChangeRef.current?.cancel();
112
+ };
113
+ }, [debounceMs, onChange]);
114
+
115
+ // Helper to handle onChange with optional debouncing
116
+ const handleChange = (value: string) => {
117
+ if (debounceMs && debouncedOnChangeRef.current) {
118
+ debouncedOnChangeRef.current(value);
119
+ } else if (onChange) {
120
+ onChange(value);
121
+ }
122
+ };
123
+
124
+ const getFieldError = (fieldName: string) => {
125
+ try {
126
+ const error = formContext?.formState?.errors?.[fieldName];
127
+ return error?.message as string | undefined;
128
+ } catch {
129
+ return undefined;
130
+ }
131
+ };
132
+
133
+ const errorMessage = getFieldError(name);
134
+ const isInvalid = !!errorMessage;
135
+
136
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
137
+ const [_textAreaValue, setTextAreaValue] = useState('');
138
+
139
+ const _handleTextAreaChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
140
+ setTextAreaValue(event.target.value);
141
+ handleChange(event.target.value);
142
+ };
143
+
144
+ useLayoutEffect(() => {
145
+ if (textareaRef.current) {
146
+ textareaRef.current.style.height = 'inherit';
147
+ textareaRef.current.style.height = `${Math.max(
148
+ textareaRef.current.scrollHeight,
149
+ MIN_TEXTAREA_HEIGHT,
150
+ )}px`;
151
+ }
152
+ }, []);
153
+
154
+ // Integrated with react-hook-form
155
+ if (formContext) {
156
+ return (
157
+ <Controller
158
+ name={name}
159
+ control={formContext.control}
160
+ rules={{
161
+ required: required ? `${label || 'This field'} is required` : false,
162
+ maxLength: maxLength
163
+ ? {
164
+ value: maxLength,
165
+ message: `Maximum ${maxLength} characters allowed`,
166
+ }
167
+ : undefined,
168
+ }}
169
+ render={({ field }) => (
170
+ <Form.Control
171
+ {...field}
172
+ onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
173
+ field.onChange(e);
174
+ setTextAreaValue(e.target.value);
175
+ handleChange(e.target.value);
176
+ }}
177
+ ref={textareaRef}
178
+ style={{
179
+ minHeight: MIN_TEXTAREA_HEIGHT,
180
+ resize: 'none',
181
+ }}
182
+ as="textarea"
183
+ required={required}
184
+ maxLength={maxLength}
185
+ size={controlSize}
186
+ placeholder={placeholder}
187
+ disabled={disabled}
188
+ readOnly={isReadOnly}
189
+ plaintext={isPlainText}
190
+ isInvalid={isInvalid}
191
+ />
192
+ )}
193
+ />
194
+ );
195
+ }
196
+
197
+ // Standalone mode
198
+ return (
199
+ <Form.Control
200
+ as="textarea"
201
+ required={required}
202
+ maxLength={maxLength}
203
+ size={controlSize}
204
+ placeholder={placeholder}
205
+ value={value || ''}
206
+ disabled={disabled}
207
+ readOnly={isReadOnly}
208
+ plaintext={isPlainText}
209
+ />
210
+ );
211
+ };