@himakarinv/nativex 1.0.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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/images.d.ts +14 -0
  4. package/index.js +8 -0
  5. package/nativewind-preset.js +18 -0
  6. package/package.json +120 -0
  7. package/src/assets/branding/logo.jpg +0 -0
  8. package/src/assets/branding/softech-logo.png +0 -0
  9. package/src/assets/icons/2-layers.svg +3 -0
  10. package/src/assets/icons/Forbidden.svg +211 -0
  11. package/src/assets/icons/Internal-server-error.svg +119 -0
  12. package/src/assets/icons/Log-out-nav.svg +4 -0
  13. package/src/assets/icons/Not-Found.svg +282 -0
  14. package/src/assets/icons/Unauthorized.svg +314 -0
  15. package/src/assets/icons/User-Frame.svg +5 -0
  16. package/src/assets/icons/alert.svg +4 -0
  17. package/src/assets/icons/arrow-right.svg +3 -0
  18. package/src/assets/icons/avatar-female.svg +10 -0
  19. package/src/assets/icons/avatar-male.svg +10 -0
  20. package/src/assets/icons/bell.svg +3 -0
  21. package/src/assets/icons/calendar-event.svg +11 -0
  22. package/src/assets/icons/calendar.svg +6 -0
  23. package/src/assets/icons/card.svg +4 -0
  24. package/src/assets/icons/caret-right.svg +3 -0
  25. package/src/assets/icons/chat.svg +11 -0
  26. package/src/assets/icons/check.svg +3 -0
  27. package/src/assets/icons/checkbox-minus.svg +3 -0
  28. package/src/assets/icons/checkmark.svg +3 -0
  29. package/src/assets/icons/chevron-down.svg +3 -0
  30. package/src/assets/icons/chevron-left.svg +9 -0
  31. package/src/assets/icons/chevron-right.svg +9 -0
  32. package/src/assets/icons/circle-radio-active.svg +10 -0
  33. package/src/assets/icons/circle-radio.svg +10 -0
  34. package/src/assets/icons/clock.svg +3 -0
  35. package/src/assets/icons/close.svg +3 -0
  36. package/src/assets/icons/code.svg +3 -0
  37. package/src/assets/icons/country.svg +20 -0
  38. package/src/assets/icons/dot.svg +3 -0
  39. package/src/assets/icons/download.svg +1 -0
  40. package/src/assets/icons/dropdown-arrow.svg +3 -0
  41. package/src/assets/icons/dropdown-avatar.svg +3 -0
  42. package/src/assets/icons/email.svg +3 -0
  43. package/src/assets/icons/error.svg +3 -0
  44. package/src/assets/icons/eye-off.svg +1 -0
  45. package/src/assets/icons/eye.svg +1 -0
  46. package/src/assets/icons/hashtag-icon.svg +3 -0
  47. package/src/assets/icons/help-circle.svg +10 -0
  48. package/src/assets/icons/help.svg +4 -0
  49. package/src/assets/icons/hide-checkbox.svg +3 -0
  50. package/src/assets/icons/home-smile.svg +3 -0
  51. package/src/assets/icons/home.svg +3 -0
  52. package/src/assets/icons/info.svg +5 -0
  53. package/src/assets/icons/left-arrow.svg +3 -0
  54. package/src/assets/icons/log-out.svg +3 -0
  55. package/src/assets/icons/notification-box.svg +4 -0
  56. package/src/assets/icons/pencil.svg +1 -0
  57. package/src/assets/icons/plus.svg +3 -0
  58. package/src/assets/icons/radio-dot.svg +3 -0
  59. package/src/assets/icons/right-arrow.svg +3 -0
  60. package/src/assets/icons/search.svg +3 -0
  61. package/src/assets/icons/settings.svg +11 -0
  62. package/src/assets/icons/slack.svg +17 -0
  63. package/src/assets/icons/slash.svg +3 -0
  64. package/src/assets/icons/success.svg +3 -0
  65. package/src/assets/icons/successicon.svg +5 -0
  66. package/src/assets/icons/three-dot-vertical.svg +5 -0
  67. package/src/assets/icons/trash.svg +1 -0
  68. package/src/assets/icons/trend-down-01.svg +3 -0
  69. package/src/assets/icons/trend-up-01.svg +3 -0
  70. package/src/assets/icons/up-arrow.svg +3 -0
  71. package/src/assets/icons/user-plus.svg +10 -0
  72. package/src/assets/icons/user.svg +3 -0
  73. package/src/assets/icons/users.svg +3 -0
  74. package/src/assets/icons/vertical-dots-true-icon.svg +5 -0
  75. package/src/assets/icons/vertical-line.svg +3 -0
  76. package/src/assets/icons/vertical-three-dots-icon.svg +5 -0
  77. package/src/assets/icons/warning.svg +3 -0
  78. package/src/assets/icons/zap.svg +3 -0
  79. package/src/components/ActionsMenu/ActionsMenu.tsx +314 -0
  80. package/src/components/ActionsMenu/index.ts +2 -0
  81. package/src/components/Alert/Alert.tsx +291 -0
  82. package/src/components/Alert/index.ts +2 -0
  83. package/src/components/Badge/Badge.tsx +230 -0
  84. package/src/components/Badge/index.ts +2 -0
  85. package/src/components/Breadcrumb/Breadcrumb.tsx +364 -0
  86. package/src/components/Breadcrumb/index.ts +7 -0
  87. package/src/components/Button/Button.tsx +584 -0
  88. package/src/components/Button/index.ts +9 -0
  89. package/src/components/Checkbox/Checkbox.tsx +336 -0
  90. package/src/components/Checkbox/index.ts +2 -0
  91. package/src/components/CreatePassword/CreatePassword.tsx +161 -0
  92. package/src/components/CreatePassword/index.ts +2 -0
  93. package/src/components/DataTable/DataTable.tsx +822 -0
  94. package/src/components/DataTable/index.ts +10 -0
  95. package/src/components/Datepicker/Datepicker.tsx +797 -0
  96. package/src/components/Datepicker/index.ts +2 -0
  97. package/src/components/Dropdown/Dropdown.tsx +277 -0
  98. package/src/components/Dropdown/index.ts +7 -0
  99. package/src/components/DropdownMenu/DropdownMenu.tsx +471 -0
  100. package/src/components/DropdownMenu/index.ts +11 -0
  101. package/src/components/DynamicForm/DynamicForm.tsx +741 -0
  102. package/src/components/DynamicForm/index.ts +13 -0
  103. package/src/components/ErrorPages/ErrorPages.tsx +135 -0
  104. package/src/components/ErrorPages/index.ts +2 -0
  105. package/src/components/ForgotPassword/ForgotPassword.tsx +121 -0
  106. package/src/components/ForgotPassword/index.ts +2 -0
  107. package/src/components/HintError/HintError.tsx +37 -0
  108. package/src/components/HintError/index.ts +2 -0
  109. package/src/components/Icon/Icon.tsx +43 -0
  110. package/src/components/Icon/icon-registry.ts +227 -0
  111. package/src/components/Icon/index.ts +3 -0
  112. package/src/components/InputDropdown/InputDropdown.tsx +662 -0
  113. package/src/components/InputDropdown/index.ts +9 -0
  114. package/src/components/InputField/InputField.tsx +527 -0
  115. package/src/components/InputField/index.ts +2 -0
  116. package/src/components/Label/Label.tsx +88 -0
  117. package/src/components/Label/index.ts +2 -0
  118. package/src/components/LoginForm/LoginForm.tsx +158 -0
  119. package/src/components/LoginForm/index.ts +2 -0
  120. package/src/components/LoginShell/LoginShell.tsx +97 -0
  121. package/src/components/LoginShell/RoleCard.tsx +24 -0
  122. package/src/components/LoginShell/SoftechLogo.tsx +24 -0
  123. package/src/components/LoginShell/branding-assets.ts +2 -0
  124. package/src/components/LoginShell/index.ts +20 -0
  125. package/src/components/LoginShell/login-copy.ts +45 -0
  126. package/src/components/LoginShell/loginPageStyles.ts +210 -0
  127. package/src/components/Modals/Modals.tsx +313 -0
  128. package/src/components/Modals/index.ts +2 -0
  129. package/src/components/Offcanvas/Offcanvas.tsx +404 -0
  130. package/src/components/Offcanvas/index.ts +13 -0
  131. package/src/components/PageHeader/PageHeader.tsx +128 -0
  132. package/src/components/PageHeader/index.ts +2 -0
  133. package/src/components/Pagination/Pagination.tsx +540 -0
  134. package/src/components/Pagination/index.ts +8 -0
  135. package/src/components/ResetPassword/ResetPassword.tsx +172 -0
  136. package/src/components/ResetPassword/index.ts +2 -0
  137. package/src/components/RoleSelection/RoleSelection.tsx +111 -0
  138. package/src/components/RoleSelection/index.ts +2 -0
  139. package/src/components/SendOtp/SendOtp.tsx +149 -0
  140. package/src/components/SendOtp/index.ts +2 -0
  141. package/src/components/Sidenavbar/Sidenavbar.tsx +358 -0
  142. package/src/components/Sidenavbar/index.ts +2 -0
  143. package/src/components/StatCard/StatCard.tsx +393 -0
  144. package/src/components/StatCard/index.ts +6 -0
  145. package/src/components/Tabs/Tabs.tsx +239 -0
  146. package/src/components/Tabs/index.ts +2 -0
  147. package/src/components/Textarea/Textarea.tsx +217 -0
  148. package/src/components/Textarea/index.ts +2 -0
  149. package/src/components/Timepicker/Timepicker.tsx +384 -0
  150. package/src/components/Timepicker/index.ts +2 -0
  151. package/src/components/Toast/Toast.tsx +278 -0
  152. package/src/components/Toast/index.ts +2 -0
  153. package/src/components/Toggle/Toggle.tsx +296 -0
  154. package/src/components/Toggle/index.ts +2 -0
  155. package/src/components/Typeahead/Typeahead.tsx +521 -0
  156. package/src/components/Typeahead/index.ts +2 -0
  157. package/src/components/Verification/Verification.tsx +374 -0
  158. package/src/components/Verification/index.ts +2 -0
  159. package/src/index.ts +39 -0
  160. package/src/theme/borders.ts +19 -0
  161. package/src/theme/button.ts +41 -0
  162. package/src/theme/colors.ts +273 -0
  163. package/src/theme/index.ts +14 -0
  164. package/src/theme/nativewind-theme.js +182 -0
  165. package/src/theme/radius.ts +12 -0
  166. package/src/theme/shadows.ts +40 -0
  167. package/src/theme/spacing.ts +18 -0
  168. package/src/theme/typography.ts +35 -0
  169. package/src/utils/a11y.ts +21 -0
  170. package/svg.d.ts +7 -0
@@ -0,0 +1,741 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ Platform,
4
+ ScrollView,
5
+ StyleSheet,
6
+ Text,
7
+ useWindowDimensions,
8
+ View,
9
+ type StyleProp,
10
+ type ViewStyle,
11
+ } from 'react-native';
12
+
13
+ import { colors, spacing, typography } from '../../theme';
14
+ import { Button, ButtonSize, ButtonVariant } from '../Button';
15
+ import { Checkbox, CheckboxShape, CheckboxSize } from '../Checkbox';
16
+ import { Datepicker, type DatepickerDate } from '../Datepicker';
17
+ import { HintError } from '../HintError';
18
+ import { InputDropdown, InputDropdownSize, type InputDropdownItem } from '../InputDropdown';
19
+ import { InputField, InputState } from '../InputField';
20
+ import { Label } from '../Label';
21
+ import {
22
+ Offcanvas,
23
+ OffcanvasPosition,
24
+ OffcanvasSize,
25
+ type OffcanvasFooterButton,
26
+ } from '../Offcanvas';
27
+ import { Textarea, TextareaVariant } from '../Textarea';
28
+ import { Timepicker, type TimepickerTime } from '../Timepicker';
29
+ import { clearToasts, showToast, Toast, ToastType } from '../Toast';
30
+ import { Typeahead, type TypeaheadOption, type TypeaheadValue } from '../Typeahead';
31
+
32
+ export const DynamicFormFieldType = {
33
+ Text: 'text',
34
+ Select: 'select',
35
+ Typeahead: 'typeahead',
36
+ Date: 'date',
37
+ Time: 'time',
38
+ Checkbox: 'checkbox',
39
+ Textarea: 'textarea',
40
+ Radio: 'radio',
41
+ } as const;
42
+
43
+ export type DynamicFormFieldType =
44
+ (typeof DynamicFormFieldType)[keyof typeof DynamicFormFieldType];
45
+
46
+ export const DynamicFormLayout = {
47
+ Stack: 'stack',
48
+ Grid: 'grid',
49
+ } as const;
50
+
51
+ export type DynamicFormLayout = (typeof DynamicFormLayout)[keyof typeof DynamicFormLayout];
52
+
53
+ export type DynamicFormSelectOption = {
54
+ label: string;
55
+ value: string;
56
+ handle?: string;
57
+ };
58
+
59
+ export type DynamicFormFieldValidation = {
60
+ required?: boolean;
61
+ minLength?: number;
62
+ maxLength?: number;
63
+ };
64
+
65
+ export type DynamicFormTypeaheadConfig = {
66
+ options?: TypeaheadOption[];
67
+ searchFn?: (query: string) => Promise<TypeaheadOption[]>;
68
+ getOptionsByIdsFn?: (ids: number[]) => Promise<TypeaheadOption[]>;
69
+ seedLabel?: string;
70
+ multiple?: boolean;
71
+ };
72
+
73
+ export type DynamicFormFieldConfig = {
74
+ key: string;
75
+ type: DynamicFormFieldType;
76
+ label: string;
77
+ placeholder?: string;
78
+ hint?: string;
79
+ defaultValue?: unknown;
80
+ disabled?: boolean;
81
+ rows?: number;
82
+ options?: DynamicFormSelectOption[];
83
+ showSuffix?: boolean;
84
+ typeahead?: DynamicFormTypeaheadConfig;
85
+ fullWidth?: boolean;
86
+ validation?: DynamicFormFieldValidation;
87
+ };
88
+
89
+ export type DynamicFormValue = Record<string, unknown>;
90
+
91
+ export type DynamicFormProps = {
92
+ fields: DynamicFormFieldConfig[];
93
+ title?: string;
94
+ description?: string;
95
+ layout?: DynamicFormLayout;
96
+ submitLabel?: string;
97
+ resetLabel?: string;
98
+ showReset?: boolean;
99
+ showActions?: boolean;
100
+ formDisabled?: boolean;
101
+ useOffcanvas?: boolean;
102
+ offcanvasTitle?: string;
103
+ offcanvasTriggerLabel?: string;
104
+ openOffcanvasOnLoad?: boolean;
105
+ offcanvasCancelLabel?: string;
106
+ offcanvasSize?: OffcanvasSize;
107
+ offcanvasPosition?: OffcanvasPosition;
108
+ showToast?: boolean;
109
+ successToastHeader?: string;
110
+ successToastMessage?: string;
111
+ errorToastHeader?: string;
112
+ errorToastMessage?: string;
113
+ onSubmitted?: (value: DynamicFormValue) => void;
114
+ onSubmitInvalid?: () => void;
115
+ onReset?: () => void;
116
+ style?: StyleProp<ViewStyle>;
117
+ };
118
+
119
+ const isNative = Platform.OS !== 'web';
120
+ const GRID_BREAKPOINT = spacing['7xl'] * 16 + spacing['6xl'] * 2;
121
+ const STACK_MAX_WIDTH = spacing['7xl'] * 8 + spacing['4xl'];
122
+ const OFFCANVAS_TITLE_ID = 'dynamic-form-offcanvas-title';
123
+
124
+ function fieldSignature(fields: DynamicFormFieldConfig[]) {
125
+ return fields.map((field) => `${field.key}:${field.type}`).join('|');
126
+ }
127
+
128
+ function defaultValueForType(type: DynamicFormFieldType): unknown {
129
+ switch (type) {
130
+ case DynamicFormFieldType.Checkbox:
131
+ return false;
132
+ case DynamicFormFieldType.Date:
133
+ case DynamicFormFieldType.Time:
134
+ case DynamicFormFieldType.Typeahead:
135
+ return null;
136
+ default:
137
+ return '';
138
+ }
139
+ }
140
+
141
+ function initialValues(fields: DynamicFormFieldConfig[]): DynamicFormValue {
142
+ const next: DynamicFormValue = {};
143
+ fields.forEach((field) => {
144
+ next[field.key] = field.defaultValue !== undefined ? field.defaultValue : defaultValueForType(field.type);
145
+ });
146
+ return next;
147
+ }
148
+
149
+ function isBlank(value: unknown) {
150
+ return value == null || value === '';
151
+ }
152
+
153
+ function isTypeaheadEmpty(value: unknown) {
154
+ if (isBlank(value)) {
155
+ return true;
156
+ }
157
+ return Array.isArray(value) && value.length === 0;
158
+ }
159
+
160
+ function fieldError(field: DynamicFormFieldConfig, value: unknown): string {
161
+ const validation = field.validation;
162
+ if (!validation) {
163
+ return '';
164
+ }
165
+
166
+ if (validation.required) {
167
+ if (field.type === DynamicFormFieldType.Checkbox && value !== true) {
168
+ return `${field.label} is required`;
169
+ }
170
+ if (field.type === DynamicFormFieldType.Typeahead && isTypeaheadEmpty(value)) {
171
+ return `${field.label} is required`;
172
+ }
173
+ if (field.type !== DynamicFormFieldType.Checkbox && field.type !== DynamicFormFieldType.Typeahead && isBlank(value)) {
174
+ return `${field.label} is required`;
175
+ }
176
+ }
177
+
178
+ const text = value == null ? '' : String(value);
179
+ if (validation.minLength != null && text.length < validation.minLength) {
180
+ return `${field.label} must be at least ${validation.minLength} characters`;
181
+ }
182
+ if (validation.maxLength != null && text.length > validation.maxLength) {
183
+ return `${field.label} must not exceed ${validation.maxLength} characters`;
184
+ }
185
+ return '';
186
+ }
187
+
188
+ function isFormInvalid(fields: DynamicFormFieldConfig[], values: DynamicFormValue) {
189
+ return fields.some((field) => fieldError(field, values[field.key]));
190
+ }
191
+
192
+ function typeaheadSearch(field: DynamicFormFieldConfig) {
193
+ const config = field.typeahead;
194
+ if (config?.searchFn) {
195
+ return config.searchFn;
196
+ }
197
+ const options = config?.options ?? [];
198
+ return (query: string) => {
199
+ const term = query.trim().toLowerCase();
200
+ if (term.length < 2) {
201
+ return Promise.resolve([]);
202
+ }
203
+ return Promise.resolve(
204
+ options.filter(
205
+ (option) =>
206
+ option.label.toLowerCase().includes(term) ||
207
+ option.email?.toLowerCase().includes(term) ||
208
+ String(option.id).includes(term),
209
+ ),
210
+ );
211
+ };
212
+ }
213
+
214
+ function typeaheadResolve(field: DynamicFormFieldConfig) {
215
+ const config = field.typeahead;
216
+ if (config?.getOptionsByIdsFn) {
217
+ return config.getOptionsByIdsFn;
218
+ }
219
+ const options = config?.options ?? [];
220
+ if (!options.length) {
221
+ return undefined;
222
+ }
223
+ return (ids: number[]) => Promise.resolve(options.filter((option) => ids.includes(Number(option.id))));
224
+ }
225
+
226
+ function selectOptions(field: DynamicFormFieldConfig): InputDropdownItem[] {
227
+ return (field.options ?? []).map((option) => ({
228
+ label: option.label,
229
+ value: option.value,
230
+ handle: option.handle,
231
+ }));
232
+ }
233
+
234
+ function selectedSelectValue(value: unknown) {
235
+ return value != null && value !== '' ? String(value) : undefined;
236
+ }
237
+
238
+ function isFullWidthField(field: DynamicFormFieldConfig) {
239
+ if (field.fullWidth != null) {
240
+ return field.fullWidth;
241
+ }
242
+ return (
243
+ field.type === DynamicFormFieldType.Textarea ||
244
+ field.type === DynamicFormFieldType.Radio ||
245
+ field.type === DynamicFormFieldType.Checkbox
246
+ );
247
+ }
248
+
249
+ export function DynamicForm({
250
+ fields,
251
+ title = '',
252
+ description = '',
253
+ layout = DynamicFormLayout.Stack,
254
+ submitLabel = 'Submit',
255
+ resetLabel = 'Reset',
256
+ showReset = true,
257
+ showActions = true,
258
+ formDisabled = false,
259
+ useOffcanvas = false,
260
+ offcanvasTitle = '',
261
+ offcanvasTriggerLabel = 'Open form',
262
+ openOffcanvasOnLoad = false,
263
+ offcanvasCancelLabel = 'Cancel',
264
+ offcanvasSize = OffcanvasSize.Medium,
265
+ offcanvasPosition = OffcanvasPosition.End,
266
+ showToast: toastEnabled = true,
267
+ successToastHeader = 'Success',
268
+ successToastMessage = 'Form saved successfully.',
269
+ errorToastHeader = 'Validation error',
270
+ errorToastMessage = 'Please complete all required fields.',
271
+ onSubmitted,
272
+ onSubmitInvalid,
273
+ onReset,
274
+ style,
275
+ }: DynamicFormProps) {
276
+ const { width } = useWindowDimensions();
277
+ const signature = useMemo(() => fieldSignature(fields), [fields]);
278
+ const [values, setValues] = useState<DynamicFormValue>(() => initialValues(fields));
279
+ const [touched, setTouched] = useState<Record<string, boolean>>({});
280
+ const [offcanvasOpen, setOffcanvasOpen] = useState(false);
281
+ const didAutoOpen = useRef(false);
282
+ const signatureRef = useRef(signature);
283
+
284
+ useEffect(() => {
285
+ if (signatureRef.current !== signature) {
286
+ signatureRef.current = signature;
287
+ setValues(initialValues(fields));
288
+ setTouched({});
289
+ }
290
+ }, [fields, signature]);
291
+
292
+ useEffect(() => {
293
+ if (useOffcanvas && openOffcanvasOnLoad && !didAutoOpen.current) {
294
+ didAutoOpen.current = true;
295
+ setOffcanvasOpen(true);
296
+ }
297
+ if (!openOffcanvasOnLoad) {
298
+ didAutoOpen.current = false;
299
+ }
300
+ }, [useOffcanvas, openOffcanvasOnLoad]);
301
+
302
+ useEffect(() => {
303
+ if (!toastEnabled) {
304
+ return;
305
+ }
306
+ return () => {
307
+ clearToasts();
308
+ };
309
+ }, [toastEnabled]);
310
+
311
+ const twoColumn = layout === DynamicFormLayout.Grid && !isNative && width >= GRID_BREAKPOINT;
312
+ const showInlineHeader = Boolean((title || description) && !useOffcanvas);
313
+ const showInlineActions = showActions && !useOffcanvas;
314
+ const resolvedOffcanvasTitle = offcanvasTitle || title || 'Form';
315
+ const invalid = isFormInvalid(fields, values);
316
+ const submitDisabled = formDisabled || invalid;
317
+
318
+ const footerButtons: OffcanvasFooterButton[] = [
319
+ {
320
+ action: 'cancel',
321
+ label: offcanvasCancelLabel,
322
+ variant: ButtonVariant.LinkGray,
323
+ },
324
+ {
325
+ action: 'save',
326
+ label: submitLabel,
327
+ variant: ButtonVariant.Primary,
328
+ disabled: formDisabled,
329
+ },
330
+ ];
331
+
332
+ function patchValue(key: string, value: unknown) {
333
+ const field = fields.find((item) => item.key === key);
334
+ if (formDisabled || field?.disabled) {
335
+ return;
336
+ }
337
+ setValues((current) => ({ ...current, [key]: value }));
338
+ setTouched((current) => ({ ...current, [key]: true }));
339
+ }
340
+
341
+ function visibleError(field: DynamicFormFieldConfig) {
342
+ if (!touched[field.key]) {
343
+ return '';
344
+ }
345
+ return fieldError(field, values[field.key]);
346
+ }
347
+
348
+ function hasError(field: DynamicFormFieldConfig) {
349
+ return Boolean(visibleError(field));
350
+ }
351
+
352
+ function isFieldDisabled(field: DynamicFormFieldConfig) {
353
+ return formDisabled || Boolean(field.disabled);
354
+ }
355
+
356
+ function onSubmit() {
357
+ const allTouched: Record<string, boolean> = {};
358
+ fields.forEach((field) => {
359
+ allTouched[field.key] = true;
360
+ });
361
+ setTouched(allTouched);
362
+
363
+ if (isFormInvalid(fields, values)) {
364
+ onSubmitInvalid?.();
365
+ if (useOffcanvas && toastEnabled) {
366
+ showToast({
367
+ type: ToastType.Error,
368
+ header: errorToastHeader,
369
+ body: errorToastMessage,
370
+ });
371
+ }
372
+ return;
373
+ }
374
+
375
+ onSubmitted?.({ ...values });
376
+
377
+ if (useOffcanvas && toastEnabled) {
378
+ showToast({
379
+ type: ToastType.Success,
380
+ header: successToastHeader,
381
+ body: successToastMessage,
382
+ });
383
+ setOffcanvasOpen(false);
384
+ }
385
+ }
386
+
387
+ function onResetForm() {
388
+ const next: DynamicFormValue = {};
389
+ fields.forEach((field) => {
390
+ next[field.key] = defaultValueForType(field.type);
391
+ });
392
+ setValues(next);
393
+ setTouched({});
394
+ onReset?.();
395
+ }
396
+
397
+ function onFooterAction(action: string) {
398
+ if (action === 'cancel') {
399
+ setOffcanvasOpen(false);
400
+ return;
401
+ }
402
+ if (action === 'save') {
403
+ onSubmit();
404
+ }
405
+ }
406
+
407
+ function renderField(field: DynamicFormFieldConfig) {
408
+ const error = visibleError(field);
409
+ const disabled = isFieldDisabled(field);
410
+ const required = Boolean(field.validation?.required);
411
+ const hint = error ? '' : (field.hint ?? '');
412
+
413
+ switch (field.type) {
414
+ case DynamicFormFieldType.Text:
415
+ return (
416
+ <InputField
417
+ fullWidth
418
+ label={field.label}
419
+ required={required}
420
+ placeholder={field.placeholder ?? ''}
421
+ hint={hint}
422
+ error={error}
423
+ state={error ? InputState.Error : InputState.Default}
424
+ value={values[field.key] != null ? String(values[field.key]) : ''}
425
+ disabled={disabled}
426
+ inputId={field.key}
427
+ onValueChange={(value) => patchValue(field.key, value)}
428
+ onBlur={() => setTouched((current) => ({ ...current, [field.key]: true }))}
429
+ />
430
+ );
431
+ case DynamicFormFieldType.Select:
432
+ return (
433
+ <View>
434
+ <InputDropdown
435
+ label={field.label}
436
+ required={required}
437
+ placeholder={field.placeholder ?? 'Select an option'}
438
+ options={selectOptions(field)}
439
+ selectedValue={selectedSelectValue(values[field.key])}
440
+ suffix={field.showSuffix ?? false}
441
+ size={InputDropdownSize.Large}
442
+ disabled={disabled}
443
+ onSelectionChange={(item) => patchValue(field.key, item?.value ?? '')}
444
+ />
445
+ <HintError hint={hint} error={error} />
446
+ </View>
447
+ );
448
+ case DynamicFormFieldType.Typeahead:
449
+ return (
450
+ <Typeahead
451
+ label={field.label}
452
+ required={required}
453
+ placeholder={field.placeholder ?? ''}
454
+ helpText={hint}
455
+ error={error}
456
+ state={error ? InputState.Error : InputState.Default}
457
+ fullWidth
458
+ inputId={field.key}
459
+ disabled={disabled}
460
+ multiple={field.typeahead?.multiple ?? false}
461
+ seedLabel={field.typeahead?.seedLabel ?? ''}
462
+ searchFn={typeaheadSearch(field)}
463
+ getOptionsByIdsFn={typeaheadResolve(field)}
464
+ value={(values[field.key] as TypeaheadValue) ?? null}
465
+ onValueChange={(value) => patchValue(field.key, value)}
466
+ />
467
+ );
468
+ case DynamicFormFieldType.Date:
469
+ return (
470
+ <View>
471
+ <Datepicker
472
+ label={field.label}
473
+ required={required}
474
+ fullWidth
475
+ date={(values[field.key] as DatepickerDate | null) ?? null}
476
+ error={error}
477
+ disabled={disabled}
478
+ onDateChange={(date) => patchValue(field.key, date)}
479
+ />
480
+ {field.hint && !hasError(field) ? <HintError hint={field.hint} /> : null}
481
+ </View>
482
+ );
483
+ case DynamicFormFieldType.Time:
484
+ return (
485
+ <View>
486
+ <Timepicker
487
+ label={field.label}
488
+ required={required}
489
+ time={(values[field.key] as TimepickerTime | null) ?? null}
490
+ disabled={disabled}
491
+ onTimeChange={(time) => patchValue(field.key, time)}
492
+ style={styles.timepicker}
493
+ />
494
+ <HintError hint={hint} error={error} />
495
+ </View>
496
+ );
497
+ case DynamicFormFieldType.Checkbox:
498
+ return (
499
+ <View>
500
+ <Checkbox
501
+ inputId={field.key}
502
+ label={field.label}
503
+ required={required}
504
+ supportingText={field.hint ?? ''}
505
+ checked={Boolean(values[field.key])}
506
+ disabled={disabled}
507
+ size={CheckboxSize.Medium}
508
+ onCheckedChange={(checked) => patchValue(field.key, checked)}
509
+ />
510
+ {error ? <HintError error={error} /> : null}
511
+ </View>
512
+ );
513
+ case DynamicFormFieldType.Textarea:
514
+ return (
515
+ <Textarea
516
+ id={field.key}
517
+ label={field.label}
518
+ required={required}
519
+ placeholder={field.placeholder ?? ''}
520
+ hint={error || (field.hint ?? '')}
521
+ variant={error ? TextareaVariant.Danger : TextareaVariant.Default}
522
+ disabled={disabled}
523
+ value={values[field.key] != null ? String(values[field.key]) : ''}
524
+ rows={field.rows ?? 4}
525
+ maxLength={field.validation?.maxLength}
526
+ onValueChange={(value) => patchValue(field.key, value)}
527
+ />
528
+ );
529
+ case DynamicFormFieldType.Radio:
530
+ return (
531
+ <View accessibilityRole="radiogroup" accessibilityLabel={field.label}>
532
+ <View style={styles.legend}>
533
+ <Label text={field.label} required={required} />
534
+ </View>
535
+ <View style={styles.radioGroup}>
536
+ {(field.options ?? []).map((option) => (
537
+ <Checkbox
538
+ key={`${field.key}-${option.value}`}
539
+ inputId={`${field.key}-${option.value}`}
540
+ label={option.label}
541
+ checked={String(values[field.key] ?? '') === option.value}
542
+ disabled={disabled}
543
+ size={CheckboxSize.Medium}
544
+ shape={CheckboxShape.Circle}
545
+ onCheckedChange={(checked) => {
546
+ if (checked) {
547
+ patchValue(field.key, option.value);
548
+ }
549
+ }}
550
+ />
551
+ ))}
552
+ </View>
553
+ <HintError hint={hint} error={error} />
554
+ </View>
555
+ );
556
+ default:
557
+ return null;
558
+ }
559
+ }
560
+
561
+ const form = (
562
+ <View
563
+ style={[
564
+ styles.form,
565
+ formDisabled && styles.formDisabled,
566
+ layout === DynamicFormLayout.Grid && styles.formGrid,
567
+ useOffcanvas && styles.formInOffcanvas,
568
+ ]}>
569
+ {showInlineHeader ? (
570
+ <View style={styles.header}>
571
+ {title ? <Text style={styles.title}>{title}</Text> : null}
572
+ {description ? <Text style={styles.description}>{description}</Text> : null}
573
+ </View>
574
+ ) : null}
575
+
576
+ <View style={[styles.fields, twoColumn && styles.fieldsGrid]}>
577
+ {fields.map((field) => (
578
+ <View
579
+ key={field.key}
580
+ style={[
581
+ styles.field,
582
+ twoColumn && styles.fieldGrid,
583
+ twoColumn && isFullWidthField(field) && styles.fieldFull,
584
+ ]}>
585
+ {renderField(field)}
586
+ </View>
587
+ ))}
588
+ </View>
589
+
590
+ {showInlineActions ? (
591
+ <View style={styles.actions}>
592
+ <Button
593
+ label={submitLabel}
594
+ variant={ButtonVariant.Primary}
595
+ size={ButtonSize.Large}
596
+ disabled={submitDisabled}
597
+ onPress={onSubmit}
598
+ />
599
+ {showReset ? (
600
+ <Button
601
+ label={resetLabel}
602
+ variant={ButtonVariant.TertiaryGray}
603
+ size={ButtonSize.Large}
604
+ disabled={formDisabled}
605
+ onPress={onResetForm}
606
+ />
607
+ ) : null}
608
+ </View>
609
+ ) : null}
610
+ </View>
611
+ );
612
+
613
+ return (
614
+ <View style={[styles.shell, style]}>
615
+ {useOffcanvas ? (
616
+ <>
617
+ <Button
618
+ label={offcanvasTriggerLabel}
619
+ variant={ButtonVariant.Primary}
620
+ size={ButtonSize.Medium}
621
+ disabled={formDisabled}
622
+ onPress={() => setOffcanvasOpen(true)}
623
+ />
624
+ {toastEnabled ? <Toast /> : null}
625
+ <Offcanvas
626
+ visible={offcanvasOpen}
627
+ title={resolvedOffcanvasTitle}
628
+ titleId={OFFCANVAS_TITLE_ID}
629
+ closeAriaLabel="Close"
630
+ footerButtons={footerButtons}
631
+ size={offcanvasSize}
632
+ position={offcanvasPosition}
633
+ onCloseRequested={() => setOffcanvasOpen(false)}
634
+ onFooterAction={onFooterAction}>
635
+ {toastEnabled ? <Toast style={styles.offcanvasToast} /> : null}
636
+ {form}
637
+ </Offcanvas>
638
+ </>
639
+ ) : (
640
+ <ScrollView
641
+ style={styles.scroll}
642
+ contentContainerStyle={styles.scrollContent}
643
+ keyboardShouldPersistTaps="handled"
644
+ nestedScrollEnabled
645
+ showsVerticalScrollIndicator>
646
+ {form}
647
+ </ScrollView>
648
+ )}
649
+ </View>
650
+ );
651
+ }
652
+
653
+ const styles = StyleSheet.create({
654
+ shell: {
655
+ width: '100%',
656
+ flex: 1,
657
+ minHeight: 0,
658
+ },
659
+ scroll: {
660
+ flex: 1,
661
+ width: '100%',
662
+ },
663
+ scrollContent: {
664
+ flexGrow: 1,
665
+ paddingBottom: spacing['7xl'],
666
+ },
667
+ timepicker: {
668
+ alignSelf: 'flex-start',
669
+ },
670
+ form: {
671
+ width: '100%',
672
+ maxWidth: isNative ? '100%' : STACK_MAX_WIDTH,
673
+ gap: spacing.xl,
674
+ },
675
+ formGrid: {
676
+ maxWidth: '100%',
677
+ },
678
+ formInOffcanvas: {
679
+ maxWidth: '100%',
680
+ },
681
+ formDisabled: {
682
+ opacity: 0.85,
683
+ },
684
+ header: {
685
+ gap: spacing.sm,
686
+ },
687
+ title: {
688
+ fontFamily: typography.fontFamily.base,
689
+ fontSize: typography.fontSize.xl,
690
+ lineHeight: typography.fontSize.xl * typography.lineHeight.tight,
691
+ fontWeight: typography.fontWeight.semibold,
692
+ color: colors.gray.darkest,
693
+ },
694
+ description: {
695
+ fontFamily: typography.fontFamily.base,
696
+ fontSize: typography.fontSize.sm,
697
+ lineHeight: typography.fontSize.sm * typography.lineHeight.normal,
698
+ color: colors.gray.dark,
699
+ },
700
+ fields: {
701
+ gap: spacing.xl,
702
+ },
703
+ fieldsGrid: {
704
+ flexDirection: 'row',
705
+ flexWrap: 'wrap',
706
+ columnGap: spacing.xl,
707
+ rowGap: spacing.lg,
708
+ },
709
+ field: {
710
+ minWidth: 0,
711
+ width: '100%',
712
+ },
713
+ fieldGrid: {
714
+ width: '48%',
715
+ flexGrow: 1,
716
+ },
717
+ fieldFull: {
718
+ width: '100%',
719
+ flexGrow: 1,
720
+ },
721
+ legend: {
722
+ marginBottom: spacing.sm,
723
+ },
724
+ radioGroup: {
725
+ gap: spacing.md,
726
+ },
727
+ actions: {
728
+ flexDirection: 'row',
729
+ flexWrap: 'nowrap',
730
+ alignItems: 'center',
731
+ gap: spacing.md,
732
+ paddingTop: spacing.sm,
733
+ },
734
+ offcanvasToast: {
735
+ position: 'relative',
736
+ top: spacing.none,
737
+ right: spacing.none,
738
+ marginBottom: spacing.md,
739
+ alignSelf: 'flex-end',
740
+ },
741
+ });