@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,33 @@
1
+ import type { StorybookConfig } from '@storybook/react-vite';
2
+
3
+ const config: StorybookConfig = {
4
+ "stories": [
5
+ "../stories/**/*.mdx",
6
+ "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)"
7
+ ],
8
+ "addons": [
9
+ "@chromatic-com/storybook",
10
+ "@storybook/addon-vitest",
11
+ "@storybook/addon-a11y",
12
+ "@storybook/addon-docs",
13
+ "@storybook/addon-onboarding"
14
+ ],
15
+ "framework": "@storybook/react-vite",
16
+ async viteFinal(config) {
17
+ // Suppress "use client" directive warnings from MUI components
18
+ if (config.build) {
19
+ config.build.rollupOptions = {
20
+ ...config.build.rollupOptions,
21
+ onwarn(warning, warn) {
22
+ // Ignore "use client" directive warnings
23
+ if (warning.code === 'MODULE_LEVEL_DIRECTIVE' && warning.message.includes('"use client"')) {
24
+ return;
25
+ }
26
+ warn(warning);
27
+ },
28
+ };
29
+ }
30
+ return config;
31
+ },
32
+ };
33
+ export default config;
@@ -0,0 +1,36 @@
1
+ import type { Preview } from '@storybook/react-vite'
2
+ import 'bootstrap/dist/css/bootstrap.min.css';
3
+ import 'react-quill-new/dist/quill.snow.css';
4
+
5
+ const preview: Preview = {
6
+ parameters: {
7
+ backgrounds: {
8
+ default: 'dark',
9
+ values: [
10
+ {
11
+ name: 'dark',
12
+ value: '#1a1a1a',
13
+ },
14
+ {
15
+ name: 'light',
16
+ value: '#ffffff',
17
+ },
18
+ ],
19
+ },
20
+ controls: {
21
+ matchers: {
22
+ color: /(background|color)$/i,
23
+ date: /Date$/i,
24
+ },
25
+ },
26
+
27
+ a11y: {
28
+ // 'todo' - show a11y violations in the test UI only
29
+ // 'error' - fail CI on a11y violations
30
+ // 'off' - skip a11y checks entirely
31
+ test: 'todo'
32
+ }
33
+ },
34
+ };
35
+
36
+ export default preview;
@@ -0,0 +1,7 @@
1
+ import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
2
+ import { setProjectAnnotations } from '@storybook/react-vite';
3
+ import * as projectAnnotations from './preview';
4
+
5
+ // This is an important step to apply the right configuration when testing your stories.
6
+ // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
7
+ setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # Components Library
2
+
3
+ A comprehensive React component library built with TypeScript, React 19, react-hook-form, react-bootstrap, and Material-UI (MUI).
4
+
5
+ ## Architecture
6
+
7
+ This library follows a clear separation between **Components** and **Addons**:
8
+
9
+ ### Components (Base Input Types)
10
+
11
+ Components are specific input elements based on HTML input types and specialized use cases:
12
+
13
+ - **CheckInput** - Checkbox input for boolean values
14
+ - **DateInput** - Date picker with formatted string output (using MUI DatePicker)
15
+ - **FileInput** - File upload with validation and preview
16
+ - **SelectInput** - Dropdown selection for single/multiple options
17
+ - **SwitchInput** - Toggle switch for on/off values
18
+ - **TagsInput** - Tag management with MUI Autocomplete
19
+ - **TextAreaInput** - Multiline text input with auto-growing height
20
+ - **TextInput** - Single-line text input with optional autocomplete
21
+
22
+ ### Addons (Enhancement Wrappers)
23
+
24
+ Addons wrap existing components to add functionality:
25
+
26
+ - **CharacterCountInput** - Adds character counting to text, textarea, or editor inputs
27
+ - **EditorAddon** - Wraps TextAreaInput to add rich text editing (ReactQuill)
28
+ - **Editor** (Legacy) - Standalone rich text editor (deprecated, use EditorAddon instead)
29
+
30
+ ## Features
31
+
32
+ - ✅ **React 19** compatible
33
+ - ✅ **TypeScript** with full type safety
34
+ - ✅ **react-hook-form** integration for form state management and validation
35
+ - ✅ **react-bootstrap** for consistent styling
36
+ - ✅ **Material-UI (MUI)** for advanced components (DatePicker, Autocomplete)
37
+ - ✅ Standalone or form-integrated modes
38
+ - ✅ Consistent error handling
39
+ - ✅ Customizable styling
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ npm install react react-dom react-hook-form react-bootstrap @mui/material @mui/x-date-pickers dayjs react-quill-new react-autosuggest
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ### Basic Component Usage
50
+
51
+ ```tsx
52
+ import { TextInput, CheckInput, DateInput } from '@your-package/components-library';
53
+ import { FormProvider, useForm } from 'react-hook-form';
54
+
55
+ function MyForm() {
56
+ const methods = useForm();
57
+
58
+ return (
59
+ <FormProvider {...methods}>
60
+ <form>
61
+ <TextInput
62
+ name='username'
63
+ label='Username'
64
+ required
65
+ maxLength={50}
66
+ />
67
+
68
+ <CheckInput
69
+ name='terms'
70
+ label='I agree to terms'
71
+ required
72
+ />
73
+
74
+ <DateInput
75
+ name='birthdate'
76
+ label='Date of Birth'
77
+ required
78
+ />
79
+ </form>
80
+ </FormProvider>
81
+ );
82
+ }
83
+ ```
84
+
85
+ ### Using Addons
86
+
87
+ #### Character Count Addon
88
+
89
+ ```tsx
90
+ import { TextInput, CharacterCountInput } from '@your-package/components-library';
91
+
92
+ function MyForm() {
93
+ return (
94
+ <CharacterCountInput>
95
+ <TextInput
96
+ name='bio'
97
+ label='Biography'
98
+ maxLength={200}
99
+ />
100
+ </CharacterCountInput>
101
+ );
102
+ }
103
+ ```
104
+
105
+ #### Editor Addon (Rich Text)
106
+
107
+ ```tsx
108
+ import { EditorAddon } from '@your-package/components-library';
109
+ import { Controller, useFormContext } from 'react-hook-form';
110
+
111
+ function MyForm() {
112
+ const { control } = useFormContext();
113
+
114
+ return (
115
+ <Controller
116
+ name='content'
117
+ control={control}
118
+ render={({ field }) => (
119
+ <EditorAddon
120
+ value={field.value}
121
+ onChange={field.onChange}
122
+ maxLength={5000}
123
+ />
124
+ )}
125
+ />
126
+ );
127
+ }
128
+ ```
129
+
130
+ #### Combining Addons
131
+
132
+ ```tsx
133
+ import { CharacterCountInput, EditorAddon } from '@your-package/components-library';
134
+
135
+ function MyForm() {
136
+ const [value, setValue] = useState('');
137
+
138
+ return (
139
+ <CharacterCountInput>
140
+ <EditorAddon
141
+ value={value}
142
+ onChange={setValue}
143
+ maxLength={1000}
144
+ />
145
+ </CharacterCountInput>
146
+ );
147
+ }
148
+ ```
149
+
150
+ ## Component API
151
+
152
+ ### Common Props
153
+
154
+ All components support these common props when used with react-hook-form:
155
+
156
+ - `name: string` (required) - Field name for form registration
157
+ - `label?: string` - Label text
158
+ - `required?: boolean` - Whether the field is required
159
+ - `disabled?: boolean` - Whether the field is disabled
160
+
161
+ ### Standalone Mode
162
+
163
+ All components can be used without react-hook-form by providing `value` and `onChange` props:
164
+
165
+ ```tsx
166
+ <TextInput
167
+ name='standalone'
168
+ value={value}
169
+ onChange={(newValue) => setValue(newValue)}
170
+ />
171
+ ```
172
+
173
+ ## Styling
174
+
175
+ Components use react-bootstrap and MUI components, making them easy to customize:
176
+
177
+ - Override Bootstrap variables for global theming
178
+ - Use MUI theme provider for MUI components
179
+ - Add custom className props (where supported)
180
+
181
+ ## TypeScript Support
182
+
183
+ All components are fully typed with TypeScript. Import types as needed:
184
+
185
+ ```tsx
186
+ import type { TextAreaInputProps, CheckInputProps } from '@your-package/components-library';
187
+ ```
188
+
189
+ ## React 19 Compatibility
190
+
191
+ This library is built and tested with React 19. All components use modern React patterns:
192
+
193
+ - Functional components with hooks
194
+ - No deprecated lifecycle methods
195
+ - No unsafe React APIs
196
+ - Full concurrent mode support
197
+
198
+ ## Contributing
199
+
200
+ When adding new components:
201
+
202
+ 1. **Components** go in `lib/components/` - base input types
203
+ 2. **Addons** go in `lib/addons/` - wrappers that enhance components
204
+ 3. All components must support both react-hook-form and standalone modes
205
+ 4. Export types alongside components
206
+ 5. Update index files for proper exports
207
+
208
+ ## License
209
+
210
+ MIT
package/biome.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
3
+ "vcs": {
4
+ "enabled": false,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": false
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": false
10
+ },
11
+ "formatter": {
12
+ "enabled": true,
13
+ "indentStyle": "tab"
14
+ },
15
+ "linter": {
16
+ "enabled": true,
17
+ "rules": {
18
+ "recommended": true,
19
+ "suspicious": {
20
+ "noExplicitAny": "warn"
21
+ }
22
+ }
23
+ },
24
+ "javascript": {
25
+ "formatter": {
26
+ "quoteStyle": "single"
27
+ }
28
+ },
29
+ "assist": {
30
+ "enabled": true,
31
+ "actions": {
32
+ "source": {
33
+ "organizeImports": "on"
34
+ }
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,204 @@
1
+ import React, {
2
+ type FC,
3
+ type PropsWithChildren,
4
+ type ReactElement,
5
+ type ReactNode,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
10
+ import { Form } from 'react-bootstrap';
11
+
12
+ const DEFAULT_VALID_HTML_ELEMENTS = [
13
+ 'text',
14
+ 'email',
15
+ 'password',
16
+ 'search',
17
+ 'tel',
18
+ 'url',
19
+ 'textarea',
20
+ ];
21
+
22
+ /**
23
+ * Props for the CharacterCountInput component
24
+ */
25
+ export type CharacterCountInputProps = PropsWithChildren<{
26
+ /** Array of valid HTML element types to monitor (e.g., ["text", "email", "textarea"]) */
27
+ validHTMLElements?: string[];
28
+ /** CSS class for the container wrapper */
29
+ containerClassName?: string;
30
+ /** CSS class for the character counter element */
31
+ counterClassName?: string;
32
+ /** CSS class applied when character count is normal */
33
+ normalClassName?: string;
34
+ /** CSS class applied when character count is near the limit */
35
+ warningClassName?: string;
36
+ /** CSS class applied when character count reaches the limit */
37
+ dangerClassName?: string;
38
+ /** Number of characters remaining before warning state (default: 10) */
39
+ warningThreshold?: number;
40
+ /** Custom function to format the counter display */
41
+ formatCounter?: (current: number, max: number) => ReactNode;
42
+ /** Whether to show the character counter (default: true) */
43
+ showCounter?: boolean;
44
+ /** Whether to show counter even when input is empty (default: false) */
45
+ showWhenEmpty?: boolean;
46
+ }>;
47
+
48
+ /**
49
+ * A wrapper component that adds a character counter to text inputs and textareas.
50
+ * Automatically detects the maxLength property from the child input and displays
51
+ * the current character count with visual feedback.
52
+ *
53
+ * Features:
54
+ * - Automatic character counting from input/textarea children
55
+ * - Visual feedback with customizable warning and danger states
56
+ * - Configurable warning threshold
57
+ * - Custom counter formatting
58
+ * - Support for multiple input types (text, email, password, textarea, etc.)
59
+ * - Show/hide counter toggle
60
+ *
61
+ * @example
62
+ * // Basic usage with TextInput
63
+ * <CharacterCountInput>
64
+ * <TextInput
65
+ * name="bio"
66
+ * type="text"
67
+ * maxLength={100}
68
+ * placeholder="Enter your bio"
69
+ * />
70
+ * </CharacterCountInput>
71
+ *
72
+ * @example
73
+ * // With custom styling and warning threshold
74
+ * <CharacterCountInput
75
+ * warningThreshold={20}
76
+ * normalClassName="text-muted"
77
+ * warningClassName="text-warning"
78
+ * dangerClassName="text-danger"
79
+ * >
80
+ * <TextAreaInput name="message" maxLength={200} />
81
+ * </CharacterCountInput>
82
+ *
83
+ * @example
84
+ * // With custom counter format
85
+ * <CharacterCountInput
86
+ * formatCounter={(current, max) => (
87
+ * <span>{max - current} characters remaining</span>
88
+ * )}
89
+ * >
90
+ * <TextInput name="title" maxLength={50} />
91
+ * </CharacterCountInput>
92
+ */
93
+ export const CharacterCountInput: FC<CharacterCountInputProps> = ({
94
+ validHTMLElements,
95
+ children,
96
+ containerClassName = 'text-end',
97
+ counterClassName = '',
98
+ normalClassName = 'text-muted',
99
+ warningClassName = 'text-warning',
100
+ dangerClassName = 'text-danger',
101
+ warningThreshold = 10,
102
+ formatCounter,
103
+ showCounter = true,
104
+ showWhenEmpty = false,
105
+ }) => {
106
+ const containerRef = useRef<HTMLDivElement>(null);
107
+ const [characterCount, setCharacterCount] = useState<number>(0);
108
+ const [maxLength, setMaxLength] = useState<number | null>(null);
109
+
110
+ useEffect(() => {
111
+ if (!containerRef.current) return;
112
+
113
+ // Extract maxLength from child props
114
+ const childProps = (
115
+ children as ReactElement<{
116
+ type?: string;
117
+ as?: string;
118
+ maxLength?: number;
119
+ }>
120
+ )?.props;
121
+
122
+ if (childProps?.maxLength) {
123
+ setMaxLength(childProps.maxLength);
124
+ }
125
+
126
+ // Use MutationObserver to wait for input element to be rendered
127
+ const findAndAttachInput = () => {
128
+ const input = containerRef.current?.querySelector('input, textarea') as
129
+ | HTMLInputElement
130
+ | HTMLTextAreaElement
131
+ | null;
132
+
133
+ if (!input) return null;
134
+
135
+ const inputReader = () => {
136
+ setCharacterCount(input.value?.length ?? 0);
137
+ };
138
+
139
+ input.addEventListener('input', inputReader);
140
+ input.addEventListener('keyup', inputReader);
141
+ inputReader(); // Initial read
142
+
143
+ return () => {
144
+ input.removeEventListener('input', inputReader);
145
+ input.removeEventListener('keyup', inputReader);
146
+ };
147
+ };
148
+
149
+ // Try to find input immediately
150
+ let cleanup = findAndAttachInput();
151
+
152
+ // If not found, wait for it to be rendered
153
+ if (!cleanup && containerRef.current) {
154
+ const observer = new MutationObserver(() => {
155
+ if (!cleanup) {
156
+ cleanup = findAndAttachInput();
157
+ if (cleanup) {
158
+ observer.disconnect();
159
+ }
160
+ }
161
+ });
162
+
163
+ observer.observe(containerRef.current, {
164
+ childList: true,
165
+ subtree: true,
166
+ });
167
+
168
+ return () => {
169
+ observer.disconnect();
170
+ cleanup?.();
171
+ };
172
+ }
173
+
174
+ return cleanup || undefined;
175
+ }, [children, validHTMLElements]);
176
+
177
+ const getCounterClassName = (): string => {
178
+ if (maxLength === null) return normalClassName;
179
+
180
+ if (characterCount === maxLength) return dangerClassName;
181
+ if (characterCount >= maxLength - warningThreshold) return warningClassName;
182
+
183
+ return normalClassName;
184
+ };
185
+
186
+ const renderCounter = (): ReactNode => {
187
+ if (!showCounter || maxLength === null) return null;
188
+ if (!showWhenEmpty && characterCount === 0) return null;
189
+
190
+ const className = `${counterClassName} ${getCounterClassName()}`.trim();
191
+ const content = formatCounter
192
+ ? formatCounter(characterCount, maxLength)
193
+ : `${characterCount} / ${maxLength}`;
194
+
195
+ return <Form.Text className={className}>{content}</Form.Text>;
196
+ };
197
+
198
+ return (
199
+ <div ref={containerRef} className={containerClassName}>
200
+ {children}
201
+ {renderCounter()}
202
+ </div>
203
+ );
204
+ };
@@ -0,0 +1,2 @@
1
+ export type { CharacterCountInputProps } from './CharacterCountInput';
2
+ export { CharacterCountInput } from './CharacterCountInput';
@@ -0,0 +1,126 @@
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 CheckInput component
7
+ */
8
+ export type CheckInputProps = {
9
+ /** The name of the checkbox input field */
10
+ name: string;
11
+ /** Label text displayed next to the checkbox */
12
+ label?: string;
13
+ /** Whether the checkbox is required */
14
+ required?: boolean;
15
+ /** Current checked state (standalone mode) */
16
+ value?: boolean;
17
+ /** Callback function called when the checked state changes */
18
+ onChange?: (checked: boolean) => void;
19
+ /** Callback function called when the checkbox loses focus */
20
+ onBlur?: () => void;
21
+ /** Whether the checkbox is disabled */
22
+ disabled?: boolean;
23
+ /** Custom HTML id for the checkbox element */
24
+ id?: string;
25
+ };
26
+
27
+ /**
28
+ * CheckInput - A checkbox input component with react-hook-form integration
29
+ *
30
+ * Provides a checkbox input that works both standalone and with react-hook-form.
31
+ * Automatically integrates with FormProvider when available, providing validation
32
+ * and error handling.
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * // With react-hook-form
37
+ * <CheckInput
38
+ * name="agreeToTerms"
39
+ * label="I agree to the terms"
40
+ * required
41
+ * />
42
+ *
43
+ * // Standalone mode
44
+ * <CheckInput
45
+ * name="newsletter"
46
+ * label="Subscribe to newsletter"
47
+ * value={subscribed}
48
+ * onChange={setSubscribed}
49
+ * />
50
+ * ```
51
+ */
52
+ export const CheckInput: FC<CheckInputProps> = ({
53
+ name,
54
+ label,
55
+ required = false,
56
+ value,
57
+ onChange,
58
+ onBlur,
59
+ disabled = false,
60
+ id,
61
+ }) => {
62
+ const formContext = useFormContext();
63
+
64
+ // Helper to safely get nested error
65
+ const getFieldError = (fieldName: string) => {
66
+ try {
67
+ const error = formContext?.formState?.errors?.[fieldName];
68
+ return error?.message as string | undefined;
69
+ } catch {
70
+ return undefined;
71
+ }
72
+ };
73
+
74
+ const errorMessage = getFieldError(name);
75
+ const inputId = id || `check-input-${name}`;
76
+
77
+ // Integrated with react-hook-form
78
+ if (formContext) {
79
+ return (
80
+ <Controller
81
+ name={name}
82
+ control={formContext.control}
83
+ rules={{
84
+ required: required ? `${label || 'This field'} is required` : false,
85
+ }}
86
+ render={({ field }) => (
87
+ <Form.Check
88
+ {...field}
89
+ id={inputId}
90
+ type="checkbox"
91
+ label={label}
92
+ checked={field.value ?? false}
93
+ onChange={(e) => {
94
+ const checked = e.target.checked;
95
+ field.onChange(checked);
96
+ onChange?.(checked);
97
+ }}
98
+ onBlur={() => {
99
+ field.onBlur();
100
+ onBlur?.();
101
+ }}
102
+ disabled={disabled}
103
+ required={required}
104
+ isInvalid={!!errorMessage}
105
+ feedback={errorMessage}
106
+ feedbackType="invalid"
107
+ />
108
+ )}
109
+ />
110
+ );
111
+ }
112
+
113
+ // Standalone mode (no form context)
114
+ return (
115
+ <Form.Check
116
+ id={inputId}
117
+ type="checkbox"
118
+ label={label}
119
+ checked={value ?? false}
120
+ onChange={(e) => onChange?.(e.target.checked)}
121
+ onBlur={onBlur}
122
+ disabled={disabled}
123
+ required={required}
124
+ />
125
+ );
126
+ };