@axenstudio/axen-form 0.1.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.
Files changed (210) hide show
  1. package/README.md +665 -0
  2. package/dist/adapters/yupAdapter.cjs +61 -0
  3. package/dist/adapters/yupAdapter.d.ts +21 -0
  4. package/dist/adapters/yupAdapter.d.ts.map +1 -0
  5. package/dist/adapters/yupAdapter.js +61 -0
  6. package/dist/adapters/zodAdapter.cjs +59 -0
  7. package/dist/adapters/zodAdapter.d.ts +22 -0
  8. package/dist/adapters/zodAdapter.d.ts.map +1 -0
  9. package/dist/adapters/zodAdapter.js +59 -0
  10. package/dist/core/dateUtils.cjs +69 -0
  11. package/dist/core/dateUtils.d.ts +28 -0
  12. package/dist/core/dateUtils.d.ts.map +1 -0
  13. package/dist/core/dateUtils.js +69 -0
  14. package/dist/core/formStore.cjs +184 -0
  15. package/dist/core/formStore.d.ts +39 -0
  16. package/dist/core/formStore.d.ts.map +1 -0
  17. package/dist/core/formStore.js +184 -0
  18. package/dist/core/numberUtils.cjs +59 -0
  19. package/dist/core/numberUtils.d.ts +26 -0
  20. package/dist/core/numberUtils.d.ts.map +1 -0
  21. package/dist/core/numberUtils.js +59 -0
  22. package/dist/core/pathUtils.cjs +44 -0
  23. package/dist/core/pathUtils.d.ts +15 -0
  24. package/dist/core/pathUtils.d.ts.map +1 -0
  25. package/dist/core/pathUtils.js +44 -0
  26. package/dist/core/payloadUtils.cjs +26 -0
  27. package/dist/core/payloadUtils.d.ts +22 -0
  28. package/dist/core/payloadUtils.d.ts.map +1 -0
  29. package/dist/core/payloadUtils.js +26 -0
  30. package/dist/core/validation.cjs +41 -0
  31. package/dist/core/validation.d.ts +7 -0
  32. package/dist/core/validation.d.ts.map +1 -0
  33. package/dist/core/validation.js +41 -0
  34. package/dist/fields/AutocompleteField/AutocompleteField.cjs +242 -0
  35. package/dist/fields/AutocompleteField/AutocompleteField.d.ts +6 -0
  36. package/dist/fields/AutocompleteField/AutocompleteField.d.ts.map +1 -0
  37. package/dist/fields/AutocompleteField/AutocompleteField.js +242 -0
  38. package/dist/fields/AutocompleteField/AutocompleteField.module.css +168 -0
  39. package/dist/fields/AutocompleteField/AutocompleteField.module.css.cjs +44 -0
  40. package/dist/fields/AutocompleteField/AutocompleteField.module.css.js +44 -0
  41. package/dist/fields/AutocompleteMultiField/AutocompleteMultiField.cjs +286 -0
  42. package/dist/fields/AutocompleteMultiField/AutocompleteMultiField.d.ts +6 -0
  43. package/dist/fields/AutocompleteMultiField/AutocompleteMultiField.d.ts.map +1 -0
  44. package/dist/fields/AutocompleteMultiField/AutocompleteMultiField.js +286 -0
  45. package/dist/fields/AutocompletePredictField/AutocompletePredictField.cjs +289 -0
  46. package/dist/fields/AutocompletePredictField/AutocompletePredictField.d.ts +10 -0
  47. package/dist/fields/AutocompletePredictField/AutocompletePredictField.d.ts.map +1 -0
  48. package/dist/fields/AutocompletePredictField/AutocompletePredictField.js +289 -0
  49. package/dist/fields/CheckboxField/CheckboxField.cjs +60 -0
  50. package/dist/fields/CheckboxField/CheckboxField.d.ts +3 -0
  51. package/dist/fields/CheckboxField/CheckboxField.d.ts.map +1 -0
  52. package/dist/fields/CheckboxField/CheckboxField.js +60 -0
  53. package/dist/fields/CheckboxField/CheckboxField.module.css +29 -0
  54. package/dist/fields/CheckboxField/CheckboxField.module.css.cjs +11 -0
  55. package/dist/fields/CheckboxField/CheckboxField.module.css.js +11 -0
  56. package/dist/fields/ColorField/ColorField.cjs +69 -0
  57. package/dist/fields/ColorField/ColorField.d.ts +3 -0
  58. package/dist/fields/ColorField/ColorField.d.ts.map +1 -0
  59. package/dist/fields/ColorField/ColorField.js +69 -0
  60. package/dist/fields/ColorField/ColorField.module.css +33 -0
  61. package/dist/fields/ColorField/ColorField.module.css.cjs +14 -0
  62. package/dist/fields/ColorField/ColorField.module.css.js +14 -0
  63. package/dist/fields/CurrencyField/CurrencyField.cjs +120 -0
  64. package/dist/fields/CurrencyField/CurrencyField.d.ts +10 -0
  65. package/dist/fields/CurrencyField/CurrencyField.d.ts.map +1 -0
  66. package/dist/fields/CurrencyField/CurrencyField.js +120 -0
  67. package/dist/fields/DateField/DateField.cjs +70 -0
  68. package/dist/fields/DateField/DateField.d.ts +7 -0
  69. package/dist/fields/DateField/DateField.d.ts.map +1 -0
  70. package/dist/fields/DateField/DateField.js +70 -0
  71. package/dist/fields/DateTimeField/DateTimeField.cjs +71 -0
  72. package/dist/fields/DateTimeField/DateTimeField.d.ts +8 -0
  73. package/dist/fields/DateTimeField/DateTimeField.d.ts.map +1 -0
  74. package/dist/fields/DateTimeField/DateTimeField.js +71 -0
  75. package/dist/fields/EmailField/EmailField.cjs +57 -0
  76. package/dist/fields/EmailField/EmailField.d.ts +3 -0
  77. package/dist/fields/EmailField/EmailField.d.ts.map +1 -0
  78. package/dist/fields/EmailField/EmailField.js +57 -0
  79. package/dist/fields/NumberField/NumberField.cjs +110 -0
  80. package/dist/fields/NumberField/NumberField.d.ts +10 -0
  81. package/dist/fields/NumberField/NumberField.d.ts.map +1 -0
  82. package/dist/fields/NumberField/NumberField.js +110 -0
  83. package/dist/fields/PasswordField/PasswordField.cjs +77 -0
  84. package/dist/fields/PasswordField/PasswordField.d.ts +3 -0
  85. package/dist/fields/PasswordField/PasswordField.d.ts.map +1 -0
  86. package/dist/fields/PasswordField/PasswordField.js +77 -0
  87. package/dist/fields/PasswordField/PasswordField.module.css +41 -0
  88. package/dist/fields/PasswordField/PasswordField.module.css.cjs +14 -0
  89. package/dist/fields/PasswordField/PasswordField.module.css.js +14 -0
  90. package/dist/fields/PhoneField/PhoneField.cjs +58 -0
  91. package/dist/fields/PhoneField/PhoneField.d.ts +3 -0
  92. package/dist/fields/PhoneField/PhoneField.d.ts.map +1 -0
  93. package/dist/fields/PhoneField/PhoneField.js +58 -0
  94. package/dist/fields/RadioField/RadioField.cjs +66 -0
  95. package/dist/fields/RadioField/RadioField.d.ts +3 -0
  96. package/dist/fields/RadioField/RadioField.d.ts.map +1 -0
  97. package/dist/fields/RadioField/RadioField.js +66 -0
  98. package/dist/fields/RadioField/RadioField.module.css +34 -0
  99. package/dist/fields/RadioField/RadioField.module.css.cjs +14 -0
  100. package/dist/fields/RadioField/RadioField.module.css.js +14 -0
  101. package/dist/fields/SelectField/SelectField.cjs +58 -0
  102. package/dist/fields/SelectField/SelectField.d.ts +3 -0
  103. package/dist/fields/SelectField/SelectField.d.ts.map +1 -0
  104. package/dist/fields/SelectField/SelectField.js +58 -0
  105. package/dist/fields/SliderField/SliderField.cjs +79 -0
  106. package/dist/fields/SliderField/SliderField.d.ts +7 -0
  107. package/dist/fields/SliderField/SliderField.d.ts.map +1 -0
  108. package/dist/fields/SliderField/SliderField.js +79 -0
  109. package/dist/fields/SliderField/SliderField.module.css +76 -0
  110. package/dist/fields/SliderField/SliderField.module.css.cjs +14 -0
  111. package/dist/fields/SliderField/SliderField.module.css.js +14 -0
  112. package/dist/fields/SwitchField/SwitchField.cjs +65 -0
  113. package/dist/fields/SwitchField/SwitchField.d.ts +3 -0
  114. package/dist/fields/SwitchField/SwitchField.d.ts.map +1 -0
  115. package/dist/fields/SwitchField/SwitchField.js +65 -0
  116. package/dist/fields/SwitchField/SwitchField.module.css +79 -0
  117. package/dist/fields/SwitchField/SwitchField.module.css.cjs +17 -0
  118. package/dist/fields/SwitchField/SwitchField.module.css.js +17 -0
  119. package/dist/fields/TextField/TextField.cjs +56 -0
  120. package/dist/fields/TextField/TextField.d.ts +3 -0
  121. package/dist/fields/TextField/TextField.d.ts.map +1 -0
  122. package/dist/fields/TextField/TextField.js +56 -0
  123. package/dist/fields/TextareaField/TextareaField.cjs +57 -0
  124. package/dist/fields/TextareaField/TextareaField.d.ts +5 -0
  125. package/dist/fields/TextareaField/TextareaField.d.ts.map +1 -0
  126. package/dist/fields/TextareaField/TextareaField.js +57 -0
  127. package/dist/fields/TimeField/TimeField.cjs +67 -0
  128. package/dist/fields/TimeField/TimeField.d.ts +6 -0
  129. package/dist/fields/TimeField/TimeField.d.ts.map +1 -0
  130. package/dist/fields/TimeField/TimeField.js +67 -0
  131. package/dist/fields/defaultComponentMap.cjs +43 -0
  132. package/dist/fields/defaultComponentMap.d.ts +7 -0
  133. package/dist/fields/defaultComponentMap.d.ts.map +1 -0
  134. package/dist/fields/defaultComponentMap.js +43 -0
  135. package/dist/fields/fieldBase.module.css +201 -0
  136. package/dist/fields/fieldBase.module.css.cjs +50 -0
  137. package/dist/fields/fieldBase.module.css.js +50 -0
  138. package/dist/fields/fieldUtils.cjs +26 -0
  139. package/dist/fields/fieldUtils.d.ts +24 -0
  140. package/dist/fields/fieldUtils.d.ts.map +1 -0
  141. package/dist/fields/fieldUtils.js +26 -0
  142. package/dist/index.cjs +99 -0
  143. package/dist/index.d.ts +42 -0
  144. package/dist/index.d.ts.map +1 -0
  145. package/dist/index.js +99 -0
  146. package/dist/layout/Box/Box.cjs +70 -0
  147. package/dist/layout/Box/Box.d.ts +6 -0
  148. package/dist/layout/Box/Box.d.ts.map +1 -0
  149. package/dist/layout/Box/Box.js +70 -0
  150. package/dist/layout/Box/Box.module.css +3 -0
  151. package/dist/layout/Box/Box.module.css.cjs +8 -0
  152. package/dist/layout/Box/Box.module.css.js +8 -0
  153. package/dist/layout/Divider/Divider.cjs +20 -0
  154. package/dist/layout/Divider/Divider.d.ts +6 -0
  155. package/dist/layout/Divider/Divider.d.ts.map +1 -0
  156. package/dist/layout/Divider/Divider.js +20 -0
  157. package/dist/layout/Divider/Divider.module.css +25 -0
  158. package/dist/layout/Divider/Divider.module.css.cjs +14 -0
  159. package/dist/layout/Divider/Divider.module.css.js +14 -0
  160. package/dist/layout/Grid/Grid.cjs +106 -0
  161. package/dist/layout/Grid/Grid.d.ts +7 -0
  162. package/dist/layout/Grid/Grid.d.ts.map +1 -0
  163. package/dist/layout/Grid/Grid.js +106 -0
  164. package/dist/layout/Grid/Grid.module.css +43 -0
  165. package/dist/layout/Grid/Grid.module.css.cjs +23 -0
  166. package/dist/layout/Grid/Grid.module.css.js +23 -0
  167. package/dist/layout/Spacer/Spacer.cjs +30 -0
  168. package/dist/layout/Spacer/Spacer.d.ts +21 -0
  169. package/dist/layout/Spacer/Spacer.d.ts.map +1 -0
  170. package/dist/layout/Spacer/Spacer.js +30 -0
  171. package/dist/layout/Stack/Stack.cjs +52 -0
  172. package/dist/layout/Stack/Stack.d.ts +7 -0
  173. package/dist/layout/Stack/Stack.d.ts.map +1 -0
  174. package/dist/layout/Stack/Stack.js +52 -0
  175. package/dist/layout/Stack/Stack.module.css +3 -0
  176. package/dist/layout/Stack/Stack.module.css.cjs +8 -0
  177. package/dist/layout/Stack/Stack.module.css.js +8 -0
  178. package/dist/layout/index.d.ts +6 -0
  179. package/dist/layout/index.d.ts.map +1 -0
  180. package/dist/react/AxenArrayField.cjs +80 -0
  181. package/dist/react/AxenArrayField.d.ts +15 -0
  182. package/dist/react/AxenArrayField.d.ts.map +1 -0
  183. package/dist/react/AxenArrayField.js +80 -0
  184. package/dist/react/AxenField.cjs +62 -0
  185. package/dist/react/AxenField.d.ts +13 -0
  186. package/dist/react/AxenField.d.ts.map +1 -0
  187. package/dist/react/AxenField.js +62 -0
  188. package/dist/react/AxenForm.cjs +134 -0
  189. package/dist/react/AxenForm.d.ts +7 -0
  190. package/dist/react/AxenForm.d.ts.map +1 -0
  191. package/dist/react/AxenForm.js +134 -0
  192. package/dist/react/FormContext.cjs +13 -0
  193. package/dist/react/FormContext.d.ts +11 -0
  194. package/dist/react/FormContext.d.ts.map +1 -0
  195. package/dist/react/FormContext.js +13 -0
  196. package/dist/react/useAxenForm.cjs +88 -0
  197. package/dist/react/useAxenForm.d.ts +8 -0
  198. package/dist/react/useAxenForm.d.ts.map +1 -0
  199. package/dist/react/useAxenForm.js +88 -0
  200. package/dist/react/useField.cjs +41 -0
  201. package/dist/react/useField.d.ts +13 -0
  202. package/dist/react/useField.d.ts.map +1 -0
  203. package/dist/react/useField.js +41 -0
  204. package/dist/react/useFieldArray.cjs +74 -0
  205. package/dist/react/useFieldArray.d.ts +5 -0
  206. package/dist/react/useFieldArray.d.ts.map +1 -0
  207. package/dist/react/useFieldArray.js +74 -0
  208. package/dist/types.d.ts +226 -0
  209. package/dist/types.d.ts.map +1 -0
  210. package/package.json +87 -0
package/README.md ADDED
@@ -0,0 +1,665 @@
1
+ # @axenstudio/axen-form
2
+
3
+ Lightweight, config-driven, enterprise-grade React form library.
4
+ Zero UI dependency. CSS Modules. A11y-aware. TypeScript strict.
5
+
6
+ [![npm](https://img.shields.io/npm/v/@axenstudio/axen-form)](https://www.npmjs.com/package/@axenstudio/axen-form)
7
+ [![license](https://img.shields.io/npm/l/@axenstudio/axen-form)](LICENSE)
8
+ [![react](https://img.shields.io/badge/react-%E2%89%A519-blue)](https://react.dev)
9
+
10
+ ---
11
+
12
+ ## Why axen-form?
13
+
14
+ | Feature | axen-form | Formik + MUI | React Hook Form |
15
+ | ------------- | ------------------------------------- | --------------------- | ---------------- |
16
+ | UI Dependency | **None** — native HTML | MUI (~350KB) | None |
17
+ | Form Engine | **Built-in** reactive store | Formik rerender-heavy | Ref-based |
18
+ | Validation | **Plugin-based** (Yup / Zod / custom) | Yup only | Resolver pattern |
19
+ | Styling | **CSS Modules** + design tokens | Emotion runtime | BYO |
20
+ | Config-Driven | **Yes** — one config = one form | No | No |
21
+ | Bundle Size | **< 15KB** gzipped | ~350KB+ with MUI | ~10KB |
22
+ | React Version | **≥ 19** | ≥ 16 | ≥ 16 |
23
+ | TypeScript | **Strict** end-to-end | Partial | Good |
24
+
25
+ ---
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install @axenstudio/axen-form
31
+ # or
32
+ pnpm add @axenstudio/axen-form
33
+ ```
34
+
35
+ **Peer dependencies:** `react ≥ 19`, `react-dom ≥ 19`
36
+
37
+ **Optional validation libraries:**
38
+
39
+ ```bash
40
+ # Pick one (or both)
41
+ pnpm add yup # Yup adapter
42
+ pnpm add zod # Zod adapter
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Quick Start
48
+
49
+ ```tsx
50
+ import { AxenForm, defaultComponentMap } from '@axenstudio/axen-form';
51
+ import '@axenstudio/axen-form/tokens';
52
+ import '@axenstudio/axen-form/reset';
53
+
54
+ const config = {
55
+ initialValues: { name: '', email: '', role: 'user' },
56
+ fields: [
57
+ { name: 'name', type: 'text', label: 'Full Name', required: true, colSpan: 6 },
58
+ { name: 'email', type: 'email', label: 'Email', required: true, colSpan: 6 },
59
+ {
60
+ name: 'role',
61
+ type: 'select',
62
+ label: 'Role',
63
+ options: [
64
+ { value: 'user', label: 'User' },
65
+ { value: 'admin', label: 'Admin' },
66
+ ],
67
+ },
68
+ ],
69
+ };
70
+
71
+ function App() {
72
+ return (
73
+ <AxenForm
74
+ config={config}
75
+ components={defaultComponentMap}
76
+ onSubmit={(values) => console.log(values)}
77
+ />
78
+ );
79
+ }
80
+ ```
81
+
82
+ That's it. One config, one component — a full form with validation, layout, and accessibility.
83
+
84
+ ---
85
+
86
+ ## 20 Built-in Field Types
87
+
88
+ ### Text Inputs
89
+
90
+ | Type | Component | Description |
91
+ | ---------- | --------------- | ---------------------------------- |
92
+ | `text` | `TextField` | Standard text input |
93
+ | `email` | `EmailField` | Email input with `type="email"` |
94
+ | `password` | `PasswordField` | Password with show/hide toggle |
95
+ | `phone` | `PhoneField` | Phone input with `type="tel"` |
96
+ | `textarea` | `TextareaField` | Multi-line text, configurable rows |
97
+
98
+ ### Numeric Inputs
99
+
100
+ | Type | Component | Description |
101
+ | ---------- | --------------- | ----------------------------------------------------- |
102
+ | `number` | `NumberField` | Locale-aware thousand separator + cursor preservation |
103
+ | `currency` | `CurrencyField` | Intl currency formatting with prefix/suffix |
104
+ | `slider` | `SliderField` | Range slider with value display |
105
+
106
+ ### Date & Time
107
+
108
+ | Type | Component | Description |
109
+ | ---------- | --------------- | ------------------------------------------------------ |
110
+ | `date` | `DateField` | Date picker — displays locale, stores ISO `YYYY-MM-DD` |
111
+ | `time` | `TimeField` | Time picker — 12h/24h, stores ISO `HH:mm` |
112
+ | `datetime` | `DateTimeField` | Combined date+time, stores ISO `YYYY-MM-DDTHH:mm` |
113
+
114
+ ### Selection
115
+
116
+ | Type | Component | Description |
117
+ | ---------- | --------------- | -------------------------- |
118
+ | `select` | `SelectField` | Native `<select>` dropdown |
119
+ | `checkbox` | `CheckboxField` | Styled checkbox with label |
120
+ | `radio` | `RadioField` | Radio group from `options` |
121
+ | `switch` | `SwitchField` | Toggle switch (on/off) |
122
+
123
+ ### Autocomplete
124
+
125
+ | Type | Component | Description |
126
+ | ---------------------- | -------------------------- | ---------------------------------- |
127
+ | `autocomplete` | `AutocompleteField` | Server-side search with debounce |
128
+ | `autocomplete-multi` | `AutocompleteMultiField` | Multi-select with chip/tag display |
129
+ | `autocomplete-predict` | `AutocompletePredictField` | Ghost text prediction + Tab accept |
130
+
131
+ ### Miscellaneous
132
+
133
+ | Type | Component | Description |
134
+ | ------- | ------------ | ------------------------ |
135
+ | `color` | `ColorField` | Color swatch + hex input |
136
+
137
+ ---
138
+
139
+ ## Core Concepts
140
+
141
+ ### Config-Driven Forms
142
+
143
+ Define your entire form with a configuration object:
144
+
145
+ ```typescript
146
+ import type { FormConfig } from '@axenstudio/axen-form';
147
+
148
+ const config: FormConfig = {
149
+ initialValues: {
150
+ name: '',
151
+ email: '',
152
+ age: 0,
153
+ birthday: '',
154
+ bio: '',
155
+ },
156
+ fields: [
157
+ { name: 'name', type: 'text', label: 'Name', required: true },
158
+ {
159
+ name: 'email',
160
+ type: 'email',
161
+ label: 'Email',
162
+ required: true,
163
+ requiredMessage: 'Email wajib diisi',
164
+ },
165
+ { name: 'age', type: 'number', label: 'Age', min: 0, max: 150, locale: 'en-US' },
166
+ { name: 'birthday', type: 'date', label: 'Birthday' },
167
+ { name: 'bio', type: 'textarea', label: 'Bio', rows: 4 },
168
+ ],
169
+ };
170
+ ```
171
+
172
+ ### Responsive Grid Layout
173
+
174
+ Fields are placed in a 12-column CSS Grid. Control column span per breakpoint:
175
+
176
+ ```typescript
177
+ {
178
+ name: 'firstName',
179
+ type: 'text',
180
+ label: 'First Name',
181
+ colSpan: { xs: 12, sm: 6, md: 4 }, // Full → half → third
182
+ }
183
+ ```
184
+
185
+ Or use a simple number for all breakpoints:
186
+
187
+ ```typescript
188
+ { name: 'email', type: 'email', label: 'Email', colSpan: 6 }
189
+ ```
190
+
191
+ ### Validation Adapters
192
+
193
+ Bring your own validation library via adapters:
194
+
195
+ **Yup:**
196
+
197
+ ```typescript
198
+ import { AxenForm, defaultComponentMap } from '@axenstudio/axen-form';
199
+ import { yupAdapter } from '@axenstudio/axen-form/adapters/yup';
200
+
201
+ <AxenForm
202
+ config={config}
203
+ components={defaultComponentMap}
204
+ validationAdapter={yupAdapter}
205
+ onSubmit={handleSubmit}
206
+ />
207
+ ```
208
+
209
+ **Zod:**
210
+
211
+ ```typescript
212
+ import { zodAdapter } from '@axenstudio/axen-form/adapters/zod';
213
+
214
+ <AxenForm
215
+ config={config}
216
+ components={defaultComponentMap}
217
+ validationAdapter={zodAdapter}
218
+ onSubmit={handleSubmit}
219
+ />
220
+ ```
221
+
222
+ **Built-in (zero-dep):**
223
+
224
+ Without any adapter, axen-form uses `simpleValidator` — validates `required` fields automatically.
225
+
226
+ ---
227
+
228
+ ## Advanced Features
229
+
230
+ ### Conditional Fields
231
+
232
+ Hide fields dynamically based on form values:
233
+
234
+ ```typescript
235
+ {
236
+ name: 'companyName',
237
+ type: 'text',
238
+ label: 'Company Name',
239
+ hidden: (values) => values.role !== 'business',
240
+ }
241
+ ```
242
+
243
+ Hidden fields are excluded from the rendered form and validation.
244
+
245
+ ### Array / Repeating Fields
246
+
247
+ ```typescript
248
+ import type { ArrayFieldConfig } from '@axenstudio/axen-form';
249
+
250
+ const contactFields: ArrayFieldConfig = {
251
+ name: 'contacts',
252
+ type: 'text', // base type (unused in array context)
253
+ isArray: true,
254
+ label: 'Contacts',
255
+ addLabel: '+ Add Contact',
256
+ removeLabel: 'Remove',
257
+ minItems: 1,
258
+ maxItems: 5,
259
+ fields: [
260
+ { name: 'name', type: 'text', label: 'Name', colSpan: 6 },
261
+ { name: 'email', type: 'email', label: 'Email', colSpan: 6 },
262
+ ],
263
+ };
264
+ ```
265
+
266
+ ### Imperative Control via Ref
267
+
268
+ ```tsx
269
+ import { useRef } from 'react';
270
+ import type { AxenFormRef } from '@axenstudio/axen-form';
271
+
272
+ function MyForm() {
273
+ const formRef = useRef<AxenFormRef>(null);
274
+
275
+ const handleExternalSubmit = () => formRef.current?.submit();
276
+ const handleReset = () => formRef.current?.resetForm();
277
+
278
+ return (
279
+ <>
280
+ <AxenForm
281
+ ref={formRef}
282
+ config={config}
283
+ components={defaultComponentMap}
284
+ onSubmit={handleSubmit}
285
+ />
286
+ <button onClick={handleExternalSubmit}>Submit from Outside</button>
287
+ <button onClick={handleReset}>Reset</button>
288
+ </>
289
+ );
290
+ }
291
+ ```
292
+
293
+ **Available ref methods:**
294
+
295
+ | Method | Return | Description |
296
+ | -------------------- | ------------------------- | --------------------------------------- |
297
+ | `submit()` | `void` | Trigger form submission |
298
+ | `resetForm(values?)` | `void` | Reset form (optionally with new values) |
299
+ | `getValues()` | `Record<string, unknown>` | Get current form values |
300
+ | `getErrors()` | `Record<string, string>` | Get current validation errors |
301
+ | `isValid()` | `boolean` | Check if form has no errors |
302
+ | `isDirty()` | `boolean` | Check if values differ from initial |
303
+
304
+ ### Payload Whitelist
305
+
306
+ Control which fields are included in the submit payload:
307
+
308
+ ```tsx
309
+ // Static whitelist
310
+ <AxenForm
311
+ config={config}
312
+ components={defaultComponentMap}
313
+ payloadFields={['name', 'email']}
314
+ onSubmit={(values) => {
315
+ // values only contains { name, email }
316
+ }}
317
+ />
318
+
319
+ // Dynamic whitelist based on discriminator field
320
+ <AxenForm
321
+ config={config}
322
+ components={defaultComponentMap}
323
+ payloadFields={{
324
+ individual: ['firstName', 'lastName', 'ssn'],
325
+ company: ['companyName', 'taxId'],
326
+ }}
327
+ payloadDiscriminator="accountType"
328
+ onSubmit={handleSubmit}
329
+ />
330
+ ```
331
+
332
+ ### Headless Usage with `useAxenForm`
333
+
334
+ For full control over rendering, use the headless hook:
335
+
336
+ ```tsx
337
+ import { useAxenForm, useField } from '@axenstudio/axen-form';
338
+
339
+ function CustomForm() {
340
+ const { store, formApi } = useAxenForm({
341
+ initialValues: { name: '' },
342
+ fields: [{ name: 'name', type: 'text', label: 'Name', required: true }],
343
+ onSubmit: async (values) => {
344
+ /* ... */
345
+ },
346
+ });
347
+
348
+ return (
349
+ <form onSubmit={formApi.handleSubmit}>
350
+ <input
351
+ name="name"
352
+ value={formApi.values.name as string}
353
+ onChange={formApi.handleChange}
354
+ onBlur={formApi.handleBlur}
355
+ />
356
+ {formApi.errors.name && <span>{formApi.errors.name}</span>}
357
+ <button type="submit">Submit</button>
358
+ </form>
359
+ );
360
+ }
361
+ ```
362
+
363
+ ### Custom Field Components
364
+
365
+ Override any built-in field by providing a custom component:
366
+
367
+ ```tsx
368
+ import type { FieldComponentProps } from '@axenstudio/axen-form';
369
+
370
+ function MyCustomSelect({ name, value, onChange, options, label, error, helperText }: FieldComponentProps) {
371
+ return (
372
+ <div>
373
+ <label>{label}</label>
374
+ {/* Your custom select UI */}
375
+ {error && <span className="error">{helperText}</span>}
376
+ </div>
377
+ );
378
+ }
379
+
380
+ // Per-field override
381
+ const config = {
382
+ initialValues: { country: '' },
383
+ fields: [
384
+ { name: 'country', type: 'select', label: 'Country', component: MyCustomSelect, options: [...] },
385
+ ],
386
+ };
387
+
388
+ // Or type-level override via components prop
389
+ <AxenForm
390
+ config={config}
391
+ components={{ ...defaultComponentMap, select: MyCustomSelect }}
392
+ onSubmit={handleSubmit}
393
+ />
394
+ ```
395
+
396
+ ---
397
+
398
+ ## Layout Components
399
+
400
+ Built-in layout components with MUI-like API, powered by CSS Grid:
401
+
402
+ ### Grid
403
+
404
+ 12-column responsive grid container:
405
+
406
+ ```tsx
407
+ import { Grid } from '@axenstudio/axen-form';
408
+
409
+ <Grid container spacing={2}>
410
+ <Grid size={{ xs: 12, md: 6 }}>
411
+ <TextField {...props} />
412
+ </Grid>
413
+ <Grid size={{ xs: 12, md: 6 }}>
414
+ <TextField {...props} />
415
+ </Grid>
416
+ </Grid>;
417
+ ```
418
+
419
+ ### Box
420
+
421
+ Generic flex/block container:
422
+
423
+ ```tsx
424
+ import { Box } from '@axenstudio/axen-form';
425
+
426
+ <Box display="flex" gap="16px" p="8px">
427
+ <span>Left</span>
428
+ <span>Right</span>
429
+ </Box>;
430
+ ```
431
+
432
+ ### Stack
433
+
434
+ Vertical/horizontal stack with gap:
435
+
436
+ ```tsx
437
+ import { Stack } from '@axenstudio/axen-form';
438
+
439
+ <Stack direction="column" spacing={2}>
440
+ <div>Item 1</div>
441
+ <div>Item 2</div>
442
+ </Stack>;
443
+ ```
444
+
445
+ ### Divider
446
+
447
+ Horizontal/vertical divider:
448
+
449
+ ```tsx
450
+ import { Divider } from '@axenstudio/axen-form';
451
+
452
+ <Stack direction="column" spacing={2}>
453
+ <div>Above</div>
454
+ <Divider />
455
+ <div>Below</div>
456
+ </Stack>;
457
+ ```
458
+
459
+ ### Spacer
460
+
461
+ Invisible spacing element — creates horizontal, vertical, or flex-basis gaps. Uses the same 8px spacing unit as Box/Grid/Stack.
462
+
463
+ ```tsx
464
+ import { Spacer } from '@axenstudio/axen-form';
465
+
466
+ // Horizontal gap (16px)
467
+ <Stack direction="row">
468
+ <Button>Save</Button>
469
+ <Spacer x={2} />
470
+ <Button>Cancel</Button>
471
+ </Stack>
472
+
473
+ // Vertical gap (24px)
474
+ <Stack direction="column">
475
+ <Card>Section A</Card>
476
+ <Spacer y={3} />
477
+ <Card>Section B</Card>
478
+ </Stack>
479
+
480
+ // Push items apart (flex grow)
481
+ <Stack direction="row">
482
+ <Logo />
483
+ <Spacer grow />
484
+ <NavLinks />
485
+ </Stack>
486
+
487
+ // Fixed flex basis (32px)
488
+ <Spacer basis={4} />
489
+ ```
490
+
491
+ | Prop | Type | Default | Description |
492
+ | --- | --- | --- | --- |
493
+ | `x` | `number` | `0` | Horizontal spacing multiplier (width = x × 8px) |
494
+ | `y` | `number` | `0` | Vertical spacing multiplier (height = y × 8px) |
495
+ | `basis` | `number` | `0` | Flex basis multiplier (basis × 8px) |
496
+ | `grow` | `boolean` | `false` | Grow to fill remaining space |
497
+
498
+ ---
499
+
500
+ ## Theming with CSS Custom Properties
501
+
502
+ axen-form uses CSS Custom Properties (design tokens) for theming. Import the default tokens and override as needed:
503
+
504
+ ```css
505
+ /* Import defaults */
506
+ @import '@axenstudio/axen-form/tokens';
507
+ @import '@axenstudio/axen-form/reset';
508
+
509
+ /* Override tokens */
510
+ :root {
511
+ --axen-primary-color: #1a73e8;
512
+ --axen-error-color: #d32f2f;
513
+ --axen-focus-color: #1a73e8;
514
+ --axen-border-radius: 8px;
515
+ --axen-font-family: 'Inter', sans-serif;
516
+ }
517
+ ```
518
+
519
+ **Key token categories:**
520
+
521
+ | Category | Example Tokens |
522
+ | ---------- | ------------------------------------------------------------------ |
523
+ | Colors | `--axen-primary-color`, `--axen-error-color`, `--axen-label-color` |
524
+ | Typography | `--axen-font-family`, `--axen-label-font-size` |
525
+ | Spacing | `--axen-field-gap`, `--axen-input-padding` |
526
+ | Borders | `--axen-border-color`, `--axen-border-radius` |
527
+ | Focus | `--axen-focus-color`, `--axen-focus-ring` |
528
+ | States | `--axen-input-disabled-bg`, `--axen-placeholder-color` |
529
+
530
+ **Built-in A11y support:**
531
+
532
+ - Dark mode via `@media (prefers-color-scheme: dark)`
533
+ - High contrast via `@media (prefers-contrast: more)`
534
+ - Reduced motion via `@media (prefers-reduced-motion: reduce)`
535
+ - Focus-visible rings for keyboard navigation
536
+
537
+ ---
538
+
539
+ ## API Reference
540
+
541
+ ### `<AxenForm>`
542
+
543
+ Main config-driven form component.
544
+
545
+ | Prop | Type | Default | Description |
546
+ | ---------------------- | -------------------------------------- | ----------------- | ------------------------------------------- |
547
+ | `config` | `FormConfig` | **required** | Form configuration (fields + initialValues) |
548
+ | `onSubmit` | `(values, meta) => void` | **required** | Submit handler |
549
+ | `components` | `FieldTypeComponentMap` | `{}` | Component map (use `defaultComponentMap`) |
550
+ | `validationAdapter` | `ValidationAdapter` | `simpleValidator` | Validation library adapter |
551
+ | `ref` | `Ref<AxenFormRef>` | — | Imperative handle |
552
+ | `columns` | `number` | `12` | Grid columns |
553
+ | `gap` | `string \| number` | — | Grid gap |
554
+ | `disabled` | `boolean` | `false` | Disable all fields |
555
+ | `className` | `string` | — | Additional CSS class |
556
+ | `payloadFields` | `string[] \| Record<string, string[]>` | — | Payload whitelist |
557
+ | `payloadDiscriminator` | `string` | — | Discriminator field for dynamic payload |
558
+ | `validateOnChange` | `boolean` | `true` | Validate on field change |
559
+ | `validateOnBlur` | `boolean` | `true` | Validate on field blur |
560
+
561
+ ### `FieldConfig`
562
+
563
+ Configuration for a single field.
564
+
565
+ | Property | Type | Description |
566
+ | -------------------- | --------------------------------------- | -------------------------------------------------- |
567
+ | `name` | `string` | Field name (supports nested paths: `address.city`) |
568
+ | `type` | `BuiltInFieldType \| string` | Field type identifier |
569
+ | `label` | `string` | Field label |
570
+ | `placeholder` | `string` | Input placeholder |
571
+ | `required` | `boolean` | Mark as required |
572
+ | `requiredMessage` | `string` | Custom required error message |
573
+ | `disabled` | `boolean` | Disable this field |
574
+ | `readOnly` | `boolean` | Read-only mode |
575
+ | `hidden` | `boolean \| (values) => boolean` | Conditional hiding |
576
+ | `component` | `ComponentType` | Override component for this field |
577
+ | `options` | `FieldOption[]` | Options for select/radio/autocomplete |
578
+ | `colSpan` | `number \| { xs?, sm?, md?, lg?, xl? }` | Grid column span |
579
+ | `helperText` | `string` | Helper text below field |
580
+ | `min`, `max`, `step` | `number` | Numeric constraints |
581
+ | `prefix`, `suffix` | `string` | Display prefix/suffix (currency) |
582
+ | `locale` | `string` | Locale for number/date formatting |
583
+ | `rows` | `number` | Textarea rows |
584
+ | `fetchOptions` | `(query, signal?) => Promise<Option[]>` | Async option fetcher (autocomplete) |
585
+ | `fieldProps` | `Record<string, unknown>` | Extra props forwarded to component |
586
+
587
+ ### Hooks
588
+
589
+ | Hook | Signature | Description |
590
+ | ---------------- | ------------------------------------------------------------------------ | ------------------------------- |
591
+ | `useAxenForm` | `(config) => { store, formApi }` | Create a headless form instance |
592
+ | `useField` | `(name) => { value, error, touched, onChange, onBlur }` | Subscribe to a single field |
593
+ | `useFieldArray` | `(name) => { items, push, remove, swap, insert, replace, move, length }` | Array field operations |
594
+ | `useFormContext` | `() => FormApi` | Access form API from context |
595
+
596
+ ### Utilities
597
+
598
+ | Utility | Description |
599
+ | --------------------------------------- | --------------------------------------- |
600
+ | `mapPayload(values, config, fields)` | Apply payload whitelist to form values |
601
+ | `pickFields(values, fieldNames)` | Pick specific fields from values object |
602
+ | `getStrippedFields(values, fieldNames)` | Get fields that would be stripped |
603
+ | `get(obj, path, default?)` | Get nested value by dot path |
604
+ | `set(obj, path, value)` | Immutable set nested value |
605
+ | `formatNumber(value, locale)` | Format number with locale |
606
+ | `formatDateLocale(isoDate, locale)` | Format ISO date for display |
607
+ | `cx(...classes)` | Conditional class name joiner |
608
+
609
+ ---
610
+
611
+ ## Architecture
612
+
613
+ ```
614
+ src/
615
+ ├── core/ # Framework-agnostic form engine
616
+ │ ├── formStore.ts # Reactive state (useSyncExternalStore compatible)
617
+ │ ├── validation.ts # Adapter interface + simpleValidator
618
+ │ ├── pathUtils.ts # Nested path get/set/toArray
619
+ │ ├── numberUtils.ts # Locale-aware number formatting
620
+ │ ├── dateUtils.ts # ISO ↔ locale date conversion
621
+ │ └── payloadUtils.ts # Payload whitelist filter
622
+ │
623
+ ├── react/ # React 19+ bindings
624
+ │ ├── AxenForm.tsx # Main config-driven renderer
625
+ │ ├── AxenField.tsx # Field wrapper component
626
+ │ ├── AxenArrayField.tsx # Array field renderer
627
+ │ ├── useAxenForm.ts # Headless form hook
628
+ │ ├── useField.ts # Single field subscription
629
+ │ ├── useFieldArray.ts # Array field operations
630
+ │ └── FormContext.ts # React context provider
631
+ │
632
+ ├── fields/ # 20 built-in field components
633
+ │ ├── defaultComponentMap.ts
634
+ │ ├── fieldUtils.ts # Shared utilities (cx, ariaProps)
635
+ │ ├── fieldBase.module.css # Shared field styles
636
+ │ ├── TextField/
637
+ │ ├── EmailField/
638
+ │ ├── ... # 18 more field directories
639
+ │ └── ColorField/
640
+ │
641
+ ├── layout/ # Grid, Box, Stack, Divider
642
+ ├── tokens/ # CSS Custom Properties + reset
643
+ ├── adapters/ # yupAdapter, zodAdapter
644
+ ├── types.ts # All TypeScript definitions
645
+ └── index.ts # Public API barrel
646
+ ```
647
+
648
+ ---
649
+
650
+ ## Browser Support
651
+
652
+ | Browser | Version |
653
+ | ------- | --------------- |
654
+ | Chrome | Last 2 versions |
655
+ | Firefox | Last 2 versions |
656
+ | Safari | 15.4+ |
657
+ | Edge | Last 2 versions |
658
+
659
+ Requires React 19+ and ES2022+ environment.
660
+
661
+ ---
662
+
663
+ ## License
664
+
665
+ MIT © [Axen Studio](https://github.com/axenstudio)