@adlas/create-app 1.0.7 → 1.0.9

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,27 @@
1
+ 'use client';
2
+
3
+ import type { ComponentProps } from 'react';
4
+
5
+ import { extendVariants, Select as HerouiSelect } from '@heroui/react';
6
+
7
+ const BaseSelect = extendVariants(HerouiSelect, {
8
+ variants: {
9
+ color: {},
10
+ isDisabled: { true: {} },
11
+ size: {
12
+ sm: {},
13
+ md: {},
14
+ lg: {},
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ size: 'md',
19
+ },
20
+ compoundVariants: [],
21
+ });
22
+
23
+ const CustomizedSelect = (props: ComponentProps<typeof BaseSelect>) => {
24
+ return <BaseSelect {...props} />;
25
+ };
26
+
27
+ export { CustomizedSelect as Select };
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+
3
+ import { extendVariants, Tabs as HerouiTabs } from '@heroui/react';
4
+
5
+ const Tabs = extendVariants(HerouiTabs, {
6
+ variants: {
7
+ color: {},
8
+ isDisabled: { true: {} },
9
+ size: {
10
+ sm: {},
11
+ md: {},
12
+ lg: {},
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ size: 'md',
17
+ },
18
+ compoundVariants: [],
19
+ });
20
+
21
+ export { Tabs };
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import type { ComponentProps, FormEvent } from 'react';
4
+
5
+ import { extendVariants, Textarea as HerouiTextArea } from '@heroui/react';
6
+
7
+ const BaseTextarea = extendVariants(HerouiTextArea, {
8
+ variants: {
9
+ color: {},
10
+ isDisabled: { true: {} },
11
+ size: {
12
+ sm: {},
13
+ md: {},
14
+ lg: {},
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ size: 'md',
19
+ },
20
+ compoundVariants: [],
21
+ });
22
+
23
+ const Textarea = (props: ComponentProps<typeof BaseTextarea>) => {
24
+ const { onInput, ...restProps } = props;
25
+
26
+ const handleInput = (e: FormEvent<HTMLInputElement>) => {
27
+ const target = e.target as HTMLTextAreaElement;
28
+
29
+ if (target.value.startsWith(' ')) {
30
+ target.value = target.value.trimStart();
31
+ }
32
+
33
+ if (onInput) {
34
+ onInput(e);
35
+ }
36
+ };
37
+
38
+ return <BaseTextarea onInput={handleInput} {...restProps} />;
39
+ };
40
+
41
+ export { Textarea };
@@ -0,0 +1,277 @@
1
+ 'use client';
2
+ import type { InputProps } from '@heroui/react';
3
+ import type { HTMLAttributes, ReactElement, ReactNode } from 'react';
4
+ import type { FieldValues, Path, Resolver, UseFormReturn } from 'react-hook-form';
5
+ import type { z } from 'zod';
6
+
7
+ import { use, useEffect, useMemo } from 'react';
8
+
9
+ import clsx from 'clsx';
10
+ import { useTranslations } from 'next-intl';
11
+ import { Controller, useForm } from 'react-hook-form';
12
+ import { z as zod } from 'zod';
13
+
14
+ import type { FormFieldValidation } from '@/validations';
15
+
16
+ import { createZodErrorMap } from '@/validations/zodErrorMap';
17
+
18
+ import type { FileValidationError } from '../DocumentUpload';
19
+
20
+ import { FormContext } from './FormContext';
21
+
22
+ /**
23
+ * Translation function type
24
+ */
25
+ type TranslationFn = (key: string, params?: Record<string, unknown>) => string;
26
+
27
+ /**
28
+ * Translates error message if it's a translation key
29
+ * Handles both simple keys and keys with parameters
30
+ */
31
+ function translateErrorMessage(message: string | undefined, t: TranslationFn): string | undefined {
32
+ if (!message) {
33
+ return undefined;
34
+ }
35
+
36
+ // Check if message is a translation key (starts with "Validations.")
37
+ if (message.includes('.') && message.startsWith('Validations.')) {
38
+ try {
39
+ return t(message);
40
+ } catch {
41
+ // If translation fails, return original message
42
+ return message;
43
+ }
44
+ }
45
+
46
+ // Return as-is if not a translation key
47
+ return message;
48
+ }
49
+
50
+ // Type-safe resolver wrapper that handles zod version compatibility
51
+ function createZodResolver<T extends FieldValues>(schema: z.ZodType<T>): Resolver<T> {
52
+ return ((_values: T, _context: object, _options: { criteriaMode?: string; names?: string[] }) => {
53
+ try {
54
+ const result = schema.safeParse(_values);
55
+
56
+ if (result.success) {
57
+ return { values: _values, errors: {} };
58
+ }
59
+
60
+ const errors: Record<string, { type: string; message: string }> = {};
61
+
62
+ result.error.issues.forEach(issue => {
63
+ const path = issue.path.join('.');
64
+
65
+ if (path) {
66
+ errors[path] = {
67
+ type: 'validation',
68
+ message: issue.message,
69
+ };
70
+ }
71
+ });
72
+
73
+ return { values: {}, errors };
74
+ } catch {
75
+ return { values: {}, errors: {} };
76
+ }
77
+ }) as Resolver<T>;
78
+ }
79
+
80
+ type FormProps<T extends FieldValues> = {
81
+ children: ReactNode;
82
+ validationSchema?: z.ZodType<T>;
83
+ form?: UseFormReturn<T>;
84
+ fieldValidation?: Record<keyof T, FormFieldValidation>;
85
+ className?: string;
86
+ } & Omit<HTMLAttributes<HTMLDivElement>, 'children'>;
87
+
88
+ export type FieldEvent = {
89
+ target?: {
90
+ value?: string | File | FileValidationError | null;
91
+ };
92
+ };
93
+
94
+ export type BaseFormFieldProps = {
95
+ onChange?: (e: FieldEvent) => void;
96
+ onBlur?: () => void;
97
+ onFocus?: () => void;
98
+ value?: string;
99
+ defaultValue?: string;
100
+ isInvalid?: boolean;
101
+ errorMessage?: string;
102
+ name?: string;
103
+ disabled?: boolean;
104
+ };
105
+
106
+ export type FormFieldProps = BaseFormFieldProps &
107
+ Partial<Omit<InputProps, keyof BaseFormFieldProps>>;
108
+
109
+ /**
110
+ * FormFieldKeys provides automatic type suggestions for form field names.
111
+ * When typing the field name as a string, you'll get autocompletion for all available fields
112
+ * defined in your form schema.
113
+ *
114
+ * Usage example:
115
+ * <FormItem name="phone_number"> // You'll get suggestions when typing
116
+ */
117
+ // export type FormFieldKeys<T extends FieldValues> = Extract<keyof T, string> & Path<T>;
118
+
119
+ export type FormItemPropsT<T extends FieldValues> = Omit<
120
+ HTMLAttributes<HTMLDivElement>,
121
+ 'children'
122
+ > & {
123
+ // Use more direct type for better IDE autocompletion
124
+ name: keyof T;
125
+ form?: UseFormReturn<T>;
126
+ label?: string;
127
+ rowsNumber?: 1 | 2;
128
+ validation?: FormFieldValidation;
129
+ overrideOnChange?: (value: string) => string;
130
+ children: (props: BaseFormFieldProps) => ReactElement;
131
+ isDisabled?: boolean;
132
+ extraLabel?: string;
133
+ };
134
+
135
+ function FormComponent<T extends FieldValues>({
136
+ children,
137
+ validationSchema,
138
+ form: externalForm,
139
+ fieldValidation,
140
+ className,
141
+ ...props
142
+ }: FormProps<T>) {
143
+ const t = useTranslations() as TranslationFn;
144
+
145
+ // Set global error map for Zod translations when component mounts
146
+ useEffect(() => {
147
+ const errorMap = createZodErrorMap(t);
148
+ zod.setErrorMap(errorMap);
149
+ }, [t]);
150
+
151
+ const internalForm = useForm<T>({
152
+ ...(validationSchema && {
153
+ resolver: createZodResolver(validationSchema),
154
+ }),
155
+ });
156
+
157
+ const form = externalForm || internalForm;
158
+
159
+ const contextValue = useMemo(() => ({ form: form as UseFormReturn<FieldValues> }), [form]);
160
+
161
+ return (
162
+ <FormContext value={contextValue}>
163
+ <div
164
+ className={clsx('grid grid-cols-10 justify-between gap-6 max-sm:gap-4', className)}
165
+ {...props}
166
+ >
167
+ {children}
168
+ </div>
169
+ </FormContext>
170
+ );
171
+ }
172
+
173
+ function FormItemComponent<T extends FieldValues>({
174
+ name,
175
+ children,
176
+ validation,
177
+ form: propForm,
178
+ className,
179
+ rowsNumber = 1,
180
+ overrideOnChange,
181
+ extraLabel,
182
+ isDisabled,
183
+ ...props
184
+ }: FormItemPropsT<T>) {
185
+ const context = use(FormContext);
186
+ const form: UseFormReturn<T> = propForm || (context?.form as UseFormReturn<T>);
187
+ const t = useTranslations() as TranslationFn;
188
+
189
+ if (!form) {
190
+ throw new Error('FormItem must be used within a Form or with a form prop');
191
+ }
192
+
193
+ // Get all available field names from the form schema
194
+ const availableFields = Object.keys(form.getValues());
195
+
196
+ // Convert name to string for validation
197
+ const nameStr = String(name);
198
+
199
+ // Validate the field name
200
+ if (!nameStr || !availableFields.includes(nameStr)) {
201
+ const suggestion = nameStr
202
+ ? availableFields.find(field => field.toLowerCase().includes(nameStr.toLowerCase()))
203
+ : null;
204
+
205
+ const errorMessage = nameStr
206
+ ? `Field name "${nameStr}" does not exist in the form schema.`
207
+ : 'Empty field name is not allowed.';
208
+
209
+ const suggestions =
210
+ availableFields.length > 0
211
+ ? `Available fields are: ${availableFields.join(', ')}`
212
+ : 'No fields available in this form.';
213
+
214
+ const suggestedField = suggestion ? `Did you mean "${suggestion}"?` : '';
215
+
216
+ console.error(`${errorMessage} ${suggestions} ${suggestedField}`);
217
+
218
+ throw new Error(`${errorMessage} ${suggestions} ${suggestedField}`);
219
+ }
220
+
221
+ const itemClassName = clsx(
222
+ 'flex flex-col',
223
+ rowsNumber === 2 ? 'col-span-5! max-xs:col-span-10!' : 'col-span-10',
224
+ className,
225
+ );
226
+
227
+ return (
228
+ <div className={itemClassName} {...props}>
229
+ {extraLabel && (
230
+ <span className="mb-2 text-sm leading-5 font-medium text-default-600">{extraLabel}</span>
231
+ )}
232
+ <Controller
233
+ control={form.control}
234
+ name={nameStr as Path<T>}
235
+ render={({ field: { onChange, disabled, onBlur, value: formValue }, fieldState }) => {
236
+ const fieldProps: BaseFormFieldProps = {
237
+ onChange: (e: FieldEvent) => {
238
+ const targetValue = e?.target?.value;
239
+
240
+ if (targetValue === null || targetValue === undefined) {
241
+ onChange('');
242
+ } else if (typeof targetValue === 'string') {
243
+ onChange(overrideOnChange ? overrideOnChange(targetValue) : targetValue);
244
+ } else if (targetValue instanceof File) {
245
+ // File type
246
+ onChange(targetValue);
247
+ } else {
248
+ // FileValidationError type
249
+ onChange(targetValue);
250
+ }
251
+ },
252
+ onBlur,
253
+ onFocus: () => {
254
+ // Trigger validation if needed
255
+ if (validation) {
256
+ form.trigger(nameStr as Path<T>);
257
+ }
258
+ },
259
+ value: (formValue as string) ?? '',
260
+ defaultValue: (formValue as string) ?? '',
261
+ isInvalid: fieldState.invalid,
262
+ errorMessage: translateErrorMessage(fieldState.error?.message, t),
263
+ name: nameStr,
264
+ disabled: isDisabled || disabled,
265
+ };
266
+
267
+ return children(fieldProps);
268
+ }}
269
+ />
270
+ </div>
271
+ );
272
+ }
273
+
274
+ FormComponent.Item = FormItemComponent;
275
+
276
+ export const Form = FormComponent;
277
+ export const FormItem = FormItemComponent;
@@ -0,0 +1,10 @@
1
+ 'use client';
2
+ import type { FieldValues, UseFormReturn } from 'react-hook-form';
3
+
4
+ import { createContext } from 'react';
5
+
6
+ export type FormContextValue<T extends FieldValues = FieldValues> = {
7
+ form: UseFormReturn<T>;
8
+ };
9
+
10
+ export const FormContext = createContext<FormContextValue<FieldValues> | null>(null);
@@ -0,0 +1,304 @@
1
+ 'use client';
2
+ import type { DatePickerProps, InputProps, Selection, SelectProps } from '@heroui/react';
3
+ import type { UseFormReturn } from 'react-hook-form';
4
+
5
+ import { AutocompleteItem, SelectItem } from '@heroui/react';
6
+
7
+ import type { BaseFormFieldProps } from './Form';
8
+
9
+ import { Autocomplete } from '../Autocomplete';
10
+ import { DatePicker } from '../DatePicker';
11
+ import { DocumentUpload } from '../DocumentUpload';
12
+ import { Input } from '../Input';
13
+ import { NumberInput } from '../NumberInput';
14
+ import { PasswordInput } from '../PasswordInput';
15
+ import { Select } from '../Select';
16
+ import { Textarea } from '../Textarea';
17
+ import { Form } from './Form';
18
+
19
+ type BaseFieldProps = {
20
+ name: string;
21
+ label?: string;
22
+ placeholder?: string;
23
+ extraLabel?: string;
24
+ className?: string;
25
+ rowsNumber?: 1 | 2;
26
+ overrideOnChange?: (value: string) => string;
27
+ };
28
+
29
+ type SelectOption = {
30
+ label: string;
31
+ value: string | number;
32
+ };
33
+
34
+ // Specific field type definitions
35
+ type FormTextInputField = {
36
+ type: 'textInput' | 'numberInput' | 'passwordInput' | 'textarea' | 'persianInput';
37
+ inputProps?: Omit<InputProps, keyof BaseFormFieldProps>;
38
+ };
39
+
40
+ type FormSelectField = {
41
+ type: 'select' | 'autocomplete';
42
+ options: SelectOption[];
43
+ selectProps?: Omit<SelectProps, keyof BaseFormFieldProps | 'children' | 'ref'>;
44
+ };
45
+
46
+ type FormDocumentUploadField = {
47
+ type: 'documentUpload';
48
+ description?: string;
49
+ required?: boolean;
50
+ accept?: string;
51
+ maxSize?: number;
52
+ multiple?: boolean;
53
+ /** If true, file will not be uploaded immediately - parent handles upload */
54
+ deferUpload?: boolean;
55
+ /** Callback when file is selected (for deferred upload mode) */
56
+ onFileSelected?: (file: File | null) => void;
57
+ };
58
+
59
+ type FormDatePickerField = {
60
+ type: 'datePicker';
61
+ datePickerProps?: Omit<DatePickerProps, keyof BaseFormFieldProps | 'onChange' | 'value'>;
62
+ };
63
+
64
+ export type FormGeneratorField = (
65
+ | FormTextInputField
66
+ | FormSelectField
67
+ | FormDocumentUploadField
68
+ | FormDatePickerField
69
+ ) &
70
+ BaseFieldProps;
71
+
72
+ export type FormGeneratorProps<
73
+ TFormValues extends Record<string, unknown> = Record<string, unknown>,
74
+ > = {
75
+ fields: FormGeneratorField[];
76
+ className?: string;
77
+ form?: UseFormReturn<TFormValues>;
78
+ onSubmit?: (data: TFormValues) => void | Promise<void>;
79
+ isDisabled?: boolean;
80
+ rowsNumber?: 1 | 2;
81
+ };
82
+
83
+ const FormGenerator = <TFormValues extends Record<string, unknown> = Record<string, unknown>>({
84
+ fields,
85
+ className,
86
+ form,
87
+ isDisabled,
88
+ rowsNumber,
89
+ }: FormGeneratorProps<TFormValues>) => {
90
+ const renderField = (field: FormGeneratorField) => {
91
+ switch (field.type) {
92
+ case 'textInput':
93
+ return (props: BaseFormFieldProps) => (
94
+ <Input
95
+ label={field.label}
96
+ placeholder={field.placeholder}
97
+ {...field.inputProps}
98
+ {...props}
99
+ ref={undefined}
100
+ isDisabled={isDisabled || field.inputProps?.isDisabled}
101
+ />
102
+ );
103
+
104
+ case 'textarea':
105
+ return (props: BaseFormFieldProps) => (
106
+ <Textarea
107
+ label={field.label}
108
+ placeholder={field.placeholder}
109
+ {...field.inputProps}
110
+ {...props}
111
+ ref={undefined}
112
+ isDisabled={isDisabled || field.inputProps?.isDisabled}
113
+ />
114
+ );
115
+
116
+ case 'numberInput':
117
+ return (props: BaseFormFieldProps) => {
118
+ const numberInputProps = {
119
+ label: field.label,
120
+ ...field.inputProps,
121
+ ...props,
122
+ isDisabled,
123
+ };
124
+
125
+ return (
126
+ <NumberInput
127
+ {...numberInputProps}
128
+ isDisabled={isDisabled || field.inputProps?.isDisabled}
129
+ />
130
+ );
131
+ };
132
+
133
+ case 'passwordInput':
134
+ return (props: BaseFormFieldProps) => (
135
+ <PasswordInput
136
+ label={field.label}
137
+ placeholder={field.placeholder}
138
+ {...field.inputProps}
139
+ {...props}
140
+ ref={undefined}
141
+ isDisabled={isDisabled || field.inputProps?.isDisabled}
142
+ />
143
+ );
144
+
145
+ case 'select': {
146
+ const { options } = field;
147
+
148
+ return (props: BaseFormFieldProps) => {
149
+ const { value, onChange, disabled } = props;
150
+
151
+ return (
152
+ <Select
153
+ label={field.label}
154
+ placeholder={field.placeholder}
155
+ aria-label={field.label || field.name}
156
+ selectedKeys={value ? [value] : []}
157
+ isDisabled={disabled || isDisabled || field.selectProps?.isDisabled}
158
+ onSelectionChange={(keys: Selection) => {
159
+ const selectedKey = Array.from(keys)[0];
160
+ onChange?.({ target: { value: selectedKey?.toString() } });
161
+ }}
162
+ >
163
+ {options.map(option => (
164
+ <SelectItem key={option.value} textValue={option.label}>
165
+ {option.label}
166
+ </SelectItem>
167
+ ))}
168
+ </Select>
169
+ );
170
+ };
171
+ }
172
+
173
+ case 'autocomplete': {
174
+ const { options, ...autocompleteProps } = field;
175
+
176
+ return (props: BaseFormFieldProps) => {
177
+ const { value, onChange, ...restProps } = props;
178
+
179
+ return (
180
+ <Autocomplete
181
+ ref={undefined}
182
+ aria-label={field.label || field.name}
183
+ label={field.label}
184
+ selectedKey={value || null}
185
+ onSelectionChange={key => {
186
+ onChange?.({ target: { value: key ? key?.toString() : '' } });
187
+ }}
188
+ {...autocompleteProps}
189
+ {...restProps}
190
+ isDisabled={isDisabled || field.selectProps?.isDisabled || props.disabled}
191
+ >
192
+ {options.map(option => (
193
+ <AutocompleteItem key={option.value} textValue={option.label}>
194
+ {option.label}
195
+ </AutocompleteItem>
196
+ ))}
197
+ </Autocomplete>
198
+ );
199
+ };
200
+ }
201
+
202
+ case 'documentUpload': {
203
+ return (props: BaseFormFieldProps) => {
204
+ const { value, onChange, ...restProps } = props;
205
+
206
+ return (
207
+ <DocumentUpload
208
+ accept={field.accept}
209
+ description={field.description}
210
+ errorMessage={restProps.errorMessage}
211
+ hasError={props.isInvalid}
212
+ label={field.label || ''}
213
+ maxSize={field.maxSize}
214
+ required={field.required}
215
+ deferUpload={field.deferUpload}
216
+ value={typeof value === 'string' ? value : null}
217
+ onChange={fileOrError => {
218
+ if (
219
+ field.deferUpload &&
220
+ fileOrError &&
221
+ typeof fileOrError === 'object' &&
222
+ 'isValidationError' in fileOrError
223
+ ) {
224
+ // Handle validation error in deferred mode
225
+ onChange?.({ target: { value: '' } });
226
+ field.onFileSelected?.(null);
227
+ } else if (field.deferUpload && fileOrError instanceof File) {
228
+ // In deferred mode, set blob URL first to satisfy validation
229
+ const blobUrl = URL.createObjectURL(fileOrError);
230
+ onChange?.({ target: { value: blobUrl } });
231
+ // Then call the callback with the file
232
+ field.onFileSelected?.(fileOrError);
233
+ } else if (field.deferUpload && fileOrError === null) {
234
+ // File was removed in deferred mode
235
+ onChange?.({ target: { value: '' } });
236
+ field.onFileSelected?.(null);
237
+ } else if (!field.deferUpload) {
238
+ // In immediate mode, don't update form value - wait for onImageUploaded
239
+ }
240
+ }}
241
+ onImageUploaded={(_imageId, imageUrl) => {
242
+ // Update form value with the uploaded image URL string (immediate mode only)
243
+ if (!field.deferUpload) {
244
+ onChange?.({ target: { value: imageUrl } });
245
+ }
246
+ }}
247
+ onError={() => {
248
+ // Clear the value on error
249
+ onChange?.({ target: { value: '' } });
250
+ }}
251
+ {...restProps}
252
+ />
253
+ );
254
+ };
255
+ }
256
+
257
+ case 'datePicker': {
258
+ return (props: BaseFormFieldProps) => {
259
+ const { value, onChange, disabled, isInvalid, errorMessage } = props;
260
+ const { color: _, ...datePickerProps } = (field.datePickerProps || {}) as Record<
261
+ string,
262
+ unknown
263
+ > & { color?: string };
264
+
265
+ return (
266
+ <DatePicker
267
+ label={field.label}
268
+ value={value || null}
269
+ onChange={dateValue => {
270
+ onChange?.({ target: { value: dateValue || '' } });
271
+ }}
272
+ isDisabled={disabled || isDisabled || field.datePickerProps?.isDisabled}
273
+ isInvalid={isInvalid}
274
+ errorMessage={errorMessage}
275
+ {...datePickerProps}
276
+ />
277
+ );
278
+ };
279
+ }
280
+
281
+ default:
282
+ return (props: BaseFormFieldProps) => <Input {...props} isDisabled={isDisabled} />;
283
+ }
284
+ };
285
+
286
+ return (
287
+ <Form className={className} form={form}>
288
+ {fields.map(field => (
289
+ <Form.Item
290
+ key={field.name}
291
+ className={field.className}
292
+ extraLabel={field.extraLabel}
293
+ name={field.name}
294
+ overrideOnChange={field.overrideOnChange}
295
+ rowsNumber={rowsNumber || field.rowsNumber || 1}
296
+ >
297
+ {renderField(field)}
298
+ </Form.Item>
299
+ ))}
300
+ </Form>
301
+ );
302
+ };
303
+
304
+ export { FormGenerator };
@@ -0,0 +1,19 @@
1
+ export * from './Autocomplete';
2
+ export * from './Breadcrumbs';
3
+ export * from './Button';
4
+ export * from './Checkbox';
5
+ export * from './Chip';
6
+ export * from './DatePicker';
7
+ export * from './DocumentUpload';
8
+ export * from './form/Form';
9
+ export * from './form/FormContext';
10
+ export * from './form/FormGenerator';
11
+ export * from './Icon';
12
+ export * from './Input';
13
+ export * from './Modal';
14
+ export * from './NumberInput';
15
+ export * from './PasswordInput';
16
+ export * from './RadioGroup';
17
+ export * from './Select';
18
+ export * from './Tabs';
19
+ export * from './Textarea';