@axenstudio/axen-form 0.1.9 → 0.1.11

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 (101) hide show
  1. package/README.md +783 -270
  2. package/dist/core/dateUtils.cjs +7 -3
  3. package/dist/core/dateUtils.d.ts +1 -1
  4. package/dist/core/dateUtils.d.ts.map +1 -1
  5. package/dist/core/dateUtils.js +7 -3
  6. package/dist/core/validation.cjs +4 -1
  7. package/dist/core/validation.d.ts.map +1 -1
  8. package/dist/core/validation.js +4 -1
  9. package/dist/fields/AutocompleteField/AutocompleteField.cjs +32 -32
  10. package/dist/fields/AutocompleteField/AutocompleteField.js +32 -32
  11. package/dist/fields/AutocompleteField/AutocompleteField.module.css.cjs +34 -13
  12. package/dist/fields/AutocompleteField/AutocompleteField.module.css.js +34 -13
  13. package/dist/fields/AutocompleteMultiField/AutocompleteMultiField.cjs +90 -88
  14. package/dist/fields/AutocompleteMultiField/AutocompleteMultiField.d.ts.map +1 -1
  15. package/dist/fields/AutocompleteMultiField/AutocompleteMultiField.js +90 -88
  16. package/dist/fields/AutocompletePredictField/AutocompletePredictField.cjs +81 -64
  17. package/dist/fields/AutocompletePredictField/AutocompletePredictField.d.ts.map +1 -1
  18. package/dist/fields/AutocompletePredictField/AutocompletePredictField.js +81 -64
  19. package/dist/fields/CheckboxField/CheckboxField.module.css.cjs +12 -0
  20. package/dist/fields/CheckboxField/CheckboxField.module.css.js +12 -0
  21. package/dist/fields/ColorField/ColorField.module.css.cjs +12 -0
  22. package/dist/fields/ColorField/ColorField.module.css.js +12 -0
  23. package/dist/fields/DateField/DateField.cjs +56 -4
  24. package/dist/fields/DateField/DateField.d.ts +1 -1
  25. package/dist/fields/DateField/DateField.d.ts.map +1 -1
  26. package/dist/fields/DateField/DateField.js +56 -4
  27. package/dist/fields/DateField/DatePicker.cjs +194 -0
  28. package/dist/fields/DateField/DatePicker.d.ts +11 -0
  29. package/dist/fields/DateField/DatePicker.d.ts.map +1 -0
  30. package/dist/fields/DateField/DatePicker.js +194 -0
  31. package/dist/fields/DateField/DatePicker.module.css.cjs +62 -0
  32. package/dist/fields/DateField/DatePicker.module.css.js +62 -0
  33. package/dist/fields/DateTimeField/DateTimeField.cjs +126 -8
  34. package/dist/fields/DateTimeField/DateTimeField.d.ts +2 -1
  35. package/dist/fields/DateTimeField/DateTimeField.d.ts.map +1 -1
  36. package/dist/fields/DateTimeField/DateTimeField.js +126 -8
  37. package/dist/fields/PasswordField/PasswordField.module.css.cjs +12 -0
  38. package/dist/fields/PasswordField/PasswordField.module.css.js +12 -0
  39. package/dist/fields/RadioField/RadioField.module.css.cjs +12 -0
  40. package/dist/fields/RadioField/RadioField.module.css.js +12 -0
  41. package/dist/fields/SliderField/SliderField.module.css.cjs +15 -3
  42. package/dist/fields/SliderField/SliderField.module.css.js +15 -3
  43. package/dist/fields/SwitchField/SwitchField.module.css.cjs +12 -0
  44. package/dist/fields/SwitchField/SwitchField.module.css.js +12 -0
  45. package/dist/fields/TimeField/TimeField.cjs +60 -5
  46. package/dist/fields/TimeField/TimeField.d.ts +2 -1
  47. package/dist/fields/TimeField/TimeField.d.ts.map +1 -1
  48. package/dist/fields/TimeField/TimeField.js +60 -5
  49. package/dist/fields/TimeField/TimePicker.cjs +182 -0
  50. package/dist/fields/TimeField/TimePicker.d.ts +11 -0
  51. package/dist/fields/TimeField/TimePicker.d.ts.map +1 -0
  52. package/dist/fields/TimeField/TimePicker.js +182 -0
  53. package/dist/fields/TimeField/TimePicker.module.css.cjs +50 -0
  54. package/dist/fields/TimeField/TimePicker.module.css.js +50 -0
  55. package/dist/fields/defaultPlaceholders.cjs +1 -0
  56. package/dist/fields/defaultPlaceholders.d.ts.map +1 -1
  57. package/dist/fields/defaultPlaceholders.js +1 -0
  58. package/dist/fields/fieldBase.module.css.cjs +366 -18
  59. package/dist/fields/fieldBase.module.css.js +366 -18
  60. package/dist/fields/icons.cjs +2 -0
  61. package/dist/fields/icons.d.ts +4 -0
  62. package/dist/fields/icons.d.ts.map +1 -1
  63. package/dist/fields/icons.js +2 -0
  64. package/dist/index.cjs +230 -3
  65. package/dist/index.d.ts +2 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +230 -3
  68. package/dist/layout/Box/Box.module.css.cjs +12 -0
  69. package/dist/layout/Box/Box.module.css.js +12 -0
  70. package/dist/layout/Divider/Divider.module.css.cjs +12 -0
  71. package/dist/layout/Divider/Divider.module.css.js +12 -0
  72. package/dist/layout/Grid/Grid.module.css.cjs +12 -0
  73. package/dist/layout/Grid/Grid.module.css.js +12 -0
  74. package/dist/layout/Stack/Stack.module.css.cjs +12 -0
  75. package/dist/layout/Stack/Stack.module.css.js +12 -0
  76. package/dist/react/AxenForm.cjs +228 -3
  77. package/dist/react/AxenForm.js +228 -3
  78. package/dist/react/fieldGroup.module.css.cjs +12 -0
  79. package/dist/react/fieldGroup.module.css.js +12 -0
  80. package/dist/react/gridCell.module.css.cjs +12 -0
  81. package/dist/react/gridCell.module.css.js +12 -0
  82. package/dist/types.d.ts +1 -0
  83. package/dist/types.d.ts.map +1 -1
  84. package/package.json +4 -9
  85. package/dist/fields/AutocompleteField/AutocompleteField.module.css +0 -168
  86. package/dist/fields/CheckboxField/CheckboxField.module.css +0 -29
  87. package/dist/fields/ColorField/ColorField.module.css +0 -33
  88. package/dist/fields/PasswordField/PasswordField.module.css +0 -41
  89. package/dist/fields/RadioField/RadioField.module.css +0 -34
  90. package/dist/fields/SliderField/SliderField.module.css +0 -76
  91. package/dist/fields/SwitchField/SwitchField.module.css +0 -79
  92. package/dist/fields/fieldBase.module.css +0 -244
  93. package/dist/layout/Box/Box.module.css +0 -3
  94. package/dist/layout/Divider/Divider.module.css +0 -25
  95. package/dist/layout/Grid/Grid.module.css +0 -43
  96. package/dist/layout/Stack/Stack.module.css +0 -3
  97. package/dist/react/fieldGroup.module.css +0 -22
  98. package/dist/react/gridCell.module.css +0 -38
  99. package/dist/tokens/reset.css +0 -68
  100. package/dist/tokens/themes.css +0 -31
  101. package/dist/tokens/tokens.css +0 -120
package/README.md CHANGED
@@ -9,6 +9,70 @@ Zero UI dependency. CSS Modules. A11y-aware. TypeScript strict.
9
9
 
10
10
  ---
11
11
 
12
+ ## Table of Contents
13
+
14
+ - [@axenstudio/axen-form](#axenstudioaxen-form)
15
+ - [Table of Contents](#table-of-contents)
16
+ - [Why axen-form?](#why-axen-form)
17
+ - [Installation](#installation)
18
+ - [Quick Start](#quick-start)
19
+ - [10 Scenarios — Basic → Expert](#10-scenarios--basic--expert)
20
+ - [BASIC](#basic)
21
+ - [01 · Basic Form](#01--basic-form)
22
+ - [02 · All 19 Field Types](#02--all-19-field-types)
23
+ - [INTERMEDIATE](#intermediate)
24
+ - [03 · Layout System](#03--layout-system)
25
+ - [04 · Yup / Zod Validation](#04--yup--zod-validation)
26
+ - [05 · Conditional / Hidden Fields](#05--conditional--hidden-fields)
27
+ - [ADVANCED](#advanced)
28
+ - [06 · Array Fields](#06--array-fields)
29
+ - [07 · Ref Control](#07--ref-control)
30
+ - [08 · Payload Mapping](#08--payload-mapping)
31
+ - [EXPERT](#expert)
32
+ - [09 · Form Context](#09--form-context)
33
+ - [10 · Custom Component](#10--custom-component)
34
+ - [Built-in Field Types (19)](#built-in-field-types-19)
35
+ - [Text Inputs](#text-inputs)
36
+ - [Numeric Inputs](#numeric-inputs)
37
+ - [Date \& Time](#date--time)
38
+ - [Selection](#selection)
39
+ - [Autocomplete](#autocomplete)
40
+ - [Miscellaneous](#miscellaneous)
41
+ - [Enterprise Features](#enterprise-features)
42
+ - [Built-in Icon System](#built-in-icon-system)
43
+ - [Default Placeholders](#default-placeholders)
44
+ - [Field Groups / Sections](#field-groups--sections)
45
+ - [Theme System](#theme-system)
46
+ - [Layout System](#layout-system)
47
+ - [Grid Layout (12-column)](#grid-layout-12-column)
48
+ - [Responsive colSpan](#responsive-colspan)
49
+ - [Layout Components](#layout-components)
50
+ - [Grid](#grid)
51
+ - [Box](#box)
52
+ - [Stack](#stack)
53
+ - [Divider](#divider)
54
+ - [Spacer](#spacer)
55
+ - [Core Concepts](#core-concepts)
56
+ - [Config-Driven Forms](#config-driven-forms)
57
+ - [Reactive Form State](#reactive-form-state)
58
+ - [Validation Adapters](#validation-adapters)
59
+ - [Advanced Features](#advanced-features)
60
+ - [Payload Whitelist](#payload-whitelist)
61
+ - [Headless Usage](#headless-usage)
62
+ - [Custom Field Components](#custom-field-components)
63
+ - [API Reference](#api-reference)
64
+ - [`<AxenForm>`](#axenform)
65
+ - [`FieldConfig`](#fieldconfig)
66
+ - [`FieldGroupConfig`](#fieldgroupconfig)
67
+ - [Hooks](#hooks)
68
+ - [Utilities](#utilities)
69
+ - [Architecture](#architecture)
70
+ - [Theming with CSS Custom Properties](#theming-with-css-custom-properties)
71
+ - [Browser Support](#browser-support)
72
+ - [License](#license)
73
+
74
+ ---
75
+
12
76
  ## Why axen-form?
13
77
 
14
78
  | Feature | axen-form | Formik + MUI | React Hook Form |
@@ -48,8 +112,6 @@ pnpm add zod # Zod adapter
48
112
 
49
113
  ```tsx
50
114
  import { AxenForm, defaultComponentMap } from '@axenstudio/axen-form';
51
- import '@axenstudio/axen-form/tokens';
52
- import '@axenstudio/axen-form/reset';
53
115
 
54
116
  const config = {
55
117
  initialValues: { name: '', email: '', role: 'user' },
@@ -79,136 +141,197 @@ function App() {
79
141
  }
80
142
  ```
81
143
 
82
- That's it. One config, one component — a full form with validation, layout, and accessibility.
144
+ That's it. One config, one component — a full form with theme, icons, placeholders, grid layout, validation, and accessibility out of the box.
83
145
 
84
146
  ---
85
147
 
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` |
148
+ ## 10 Scenarios — Basic → Expert
113
149
 
114
- ### Selection
150
+ axen-form is designed to scale from a quick contact form to a complex enterprise workflow. The following 10 scenarios illustrate the full capability range, from basic to expert-level usage.
115
151
 
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) |
152
+ ### BASIC
122
153
 
123
- ### Autocomplete
154
+ #### 01 · Basic Form
124
155
 
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 |
156
+ The simplest form: a few fields, default layout, built-in validation.
130
157
 
131
- ### Miscellaneous
132
-
133
- | Type | Component | Description |
134
- | ------- | ------------ | ------------------------ |
135
- | `color` | `ColorField` | Color swatch + hex input |
158
+ ```tsx
159
+ import { AxenForm, defaultComponentMap } from '@axenstudio/axen-form';
136
160
 
137
- ---
161
+ const config = {
162
+ initialValues: { name: '', email: '' },
163
+ fields: [
164
+ { name: 'name', type: 'text', label: 'Name', required: true },
165
+ { name: 'email', type: 'email', label: 'Email', required: true },
166
+ ],
167
+ };
138
168
 
139
- ## Core Concepts
169
+ <AxenForm config={config} components={defaultComponentMap} onSubmit={console.log} />;
170
+ ```
140
171
 
141
- ### Config-Driven Forms
172
+ - Fields are full-width by default (colSpan = 12)
173
+ - Built-in `simpleValidator` validates `required` fields
174
+ - Enterprise styling applied automatically (icons, tinted inputs, themed labels)
142
175
 
143
- Define your entire form with a configuration object:
176
+ #### 02 · All 19 Field Types
144
177
 
145
- ```typescript
146
- import type { FormConfig } from '@axenstudio/axen-form';
178
+ Render every built-in field type in a single form to explore the full component palette.
147
179
 
148
- const config: FormConfig = {
180
+ ```tsx
181
+ const config = {
149
182
  initialValues: {
150
183
  name: '',
151
184
  email: '',
185
+ password: '',
186
+ phone: '',
187
+ bio: '',
152
188
  age: 0,
189
+ price: 0,
190
+ rating: 50,
153
191
  birthday: '',
154
- bio: '',
192
+ alarm: '',
193
+ meeting: '',
194
+ country: '',
195
+ agree: false,
196
+ priority: 'medium',
197
+ darkMode: false,
198
+ city: '',
199
+ tags: [],
200
+ suggestion: '',
201
+ color: '#1976d2',
155
202
  },
156
203
  fields: [
157
- { name: 'name', type: 'text', label: 'Name', required: true },
204
+ // Text inputs
205
+ { name: 'name', type: 'text', label: 'Full Name', colSpan: 6 },
206
+ { name: 'email', type: 'email', label: 'Email', colSpan: 6 },
207
+ { name: 'password', type: 'password', label: 'Password', colSpan: 6 },
208
+ { name: 'phone', type: 'phone', label: 'Phone', colSpan: 6 },
209
+ { name: 'bio', type: 'textarea', label: 'Biography', rows: 3 },
210
+
211
+ // Numeric
212
+ { name: 'age', type: 'number', label: 'Age', min: 0, max: 150, colSpan: 4 },
158
213
  {
159
- name: 'email',
160
- type: 'email',
161
- label: 'Email',
162
- required: true,
163
- requiredMessage: 'Email wajib diisi',
214
+ name: 'price',
215
+ type: 'currency',
216
+ label: 'Price',
217
+ currency: 'USD',
218
+ locale: 'en-US',
219
+ colSpan: 4,
164
220
  },
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 },
221
+ { name: 'rating', type: 'slider', label: 'Rating', min: 0, max: 100, step: 5, colSpan: 4 },
222
+
223
+ // Date & Time
224
+ { name: 'birthday', type: 'date', label: 'Birthday', colSpan: 4 },
225
+ { name: 'alarm', type: 'time', label: 'Alarm', ampm: true, colSpan: 4 },
226
+ { name: 'meeting', type: 'datetime', label: 'Meeting', colSpan: 4 },
227
+
228
+ // Selection
229
+ {
230
+ name: 'country',
231
+ type: 'select',
232
+ label: 'Country',
233
+ options: [
234
+ { value: 'us', label: 'United States' },
235
+ { value: 'id', label: 'Indonesia' },
236
+ ],
237
+ colSpan: 6,
238
+ },
239
+ {
240
+ name: 'priority',
241
+ type: 'radio',
242
+ label: 'Priority',
243
+ options: [
244
+ { value: 'low', label: 'Low' },
245
+ { value: 'medium', label: 'Medium' },
246
+ { value: 'high', label: 'High' },
247
+ ],
248
+ colSpan: 6,
249
+ },
250
+ { name: 'agree', type: 'checkbox', label: 'I agree to terms', colSpan: 6 },
251
+ { name: 'darkMode', type: 'switch', label: 'Dark Mode', colSpan: 6 },
252
+
253
+ // Autocomplete
254
+ { name: 'city', type: 'autocomplete', label: 'City', fetchOptions: searchCities, colSpan: 4 },
255
+ { name: 'tags', type: 'autocomplete-multi', label: 'Tags', options: tagOptions, colSpan: 4 },
256
+ {
257
+ name: 'suggestion',
258
+ type: 'autocomplete-predict',
259
+ label: 'Suggestion',
260
+ fetchOptions: fetchSuggestions,
261
+ colSpan: 4,
262
+ },
263
+
264
+ // Miscellaneous
265
+ { name: 'color', type: 'color', label: 'Theme Color', colSpan: 4 },
168
266
  ],
169
267
  };
170
268
  ```
171
269
 
172
- ### Responsive Grid Layout
270
+ ### INTERMEDIATE
173
271
 
174
- Fields are placed in a 12-column CSS Grid. Control column span per breakpoint:
272
+ #### 03 · Layout System
175
273
 
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
- ```
274
+ Control column spans per breakpoint for responsive form layouts.
184
275
 
185
- Or use a simple number for all breakpoints:
276
+ ```tsx
277
+ const config = {
278
+ initialValues: { firstName: '', lastName: '', email: '', address: '', city: '', zip: '' },
279
+ fields: [
280
+ // Side-by-side on tablet+, stacked on mobile
281
+ { name: 'firstName', type: 'text', label: 'First Name', colSpan: { xs: 12, sm: 6 } },
282
+ { name: 'lastName', type: 'text', label: 'Last Name', colSpan: { xs: 12, sm: 6 } },
283
+ // Full width
284
+ { name: 'email', type: 'email', label: 'Email' },
285
+ { name: 'address', type: 'text', label: 'Address' },
286
+ // Three across on desktop
287
+ { name: 'city', type: 'text', label: 'City', colSpan: { xs: 12, sm: 6, md: 4 } },
288
+ {
289
+ name: 'state',
290
+ type: 'select',
291
+ label: 'State',
292
+ colSpan: { xs: 12, sm: 6, md: 4 },
293
+ options: stateOptions,
294
+ },
295
+ { name: 'zip', type: 'text', label: 'ZIP', colSpan: { xs: 12, md: 4 } },
296
+ ],
297
+ };
186
298
 
187
- ```typescript
188
- { name: 'email', type: 'email', label: 'Email', colSpan: 6 }
299
+ // colSpan breakpoints: xs (<600px), sm (≥600px), md (≥900px), lg (≥1200px), xl (≥1536px)
189
300
  ```
190
301
 
191
- ### Validation Adapters
302
+ See [Layout System](#layout-system) for `Grid`, `Box`, `Stack`, `Divider`, and `Spacer` components.
192
303
 
193
- Bring your own validation library via adapters:
304
+ #### 04 · Yup / Zod Validation
194
305
 
195
- **Yup:**
306
+ Bring your own validation library via adapters.
196
307
 
197
- ```typescript
198
- import { AxenForm, defaultComponentMap } from '@axenstudio/axen-form';
308
+ **Yup Adapter:**
309
+
310
+ ```tsx
199
311
  import { yupAdapter } from '@axenstudio/axen-form/adapters/yup';
312
+ import * as yup from 'yup';
313
+
314
+ // Fields with required: true auto-generate schema.
315
+ // For custom rules, use the validation prop:
316
+ const config = {
317
+ initialValues: { email: '', age: 0 },
318
+ fields: [
319
+ { name: 'email', type: 'email', label: 'Email', required: true },
320
+ { name: 'age', type: 'number', label: 'Age', required: true, min: 18, max: 120 },
321
+ ],
322
+ };
200
323
 
201
324
  <AxenForm
202
325
  config={config}
203
326
  components={defaultComponentMap}
204
327
  validationAdapter={yupAdapter}
205
328
  onSubmit={handleSubmit}
206
- />
329
+ />;
207
330
  ```
208
331
 
209
- **Zod:**
332
+ **Zod Adapter:**
210
333
 
211
- ```typescript
334
+ ```tsx
212
335
  import { zodAdapter } from '@axenstudio/axen-form/adapters/zod';
213
336
 
214
337
  <AxenForm
@@ -216,65 +339,97 @@ import { zodAdapter } from '@axenstudio/axen-form/adapters/zod';
216
339
  components={defaultComponentMap}
217
340
  validationAdapter={zodAdapter}
218
341
  onSubmit={handleSubmit}
219
- />
342
+ />;
220
343
  ```
221
344
 
222
345
  **Built-in (zero-dep):**
223
346
 
224
347
  Without any adapter, axen-form uses `simpleValidator` — validates `required` fields automatically.
225
348
 
226
- ---
227
-
228
- ## Advanced Features
349
+ #### 05 · Conditional / Hidden Fields
229
350
 
230
- ### Conditional Fields
351
+ Hide fields dynamically based on form values. Hidden fields are excluded from validation and rendering.
231
352
 
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
- }
353
+ ```tsx
354
+ const config = {
355
+ initialValues: { accountType: 'personal', companyName: '', taxId: '' },
356
+ fields: [
357
+ {
358
+ name: 'accountType',
359
+ type: 'select',
360
+ label: 'Account Type',
361
+ options: [
362
+ { value: 'personal', label: 'Personal' },
363
+ { value: 'business', label: 'Business' },
364
+ ],
365
+ },
366
+ {
367
+ name: 'companyName',
368
+ type: 'text',
369
+ label: 'Company Name',
370
+ required: true,
371
+ hidden: (values) => values.accountType !== 'business',
372
+ },
373
+ {
374
+ name: 'taxId',
375
+ type: 'text',
376
+ label: 'Tax ID',
377
+ hidden: (values) => values.accountType !== 'business',
378
+ },
379
+ ],
380
+ };
241
381
  ```
242
382
 
243
- Hidden fields are excluded from the rendered form and validation.
383
+ ### ADVANCED
244
384
 
245
- ### Array / Repeating Fields
385
+ #### 06 · Array Fields
246
386
 
247
- ```typescript
387
+ Repeating field groups with add/remove controls.
388
+
389
+ ```tsx
248
390
  import type { ArrayFieldConfig } from '@axenstudio/axen-form';
249
391
 
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,
392
+ const config = {
393
+ initialValues: { contacts: [{ name: '', email: '' }] },
259
394
  fields: [
260
- { name: 'name', type: 'text', label: 'Name', colSpan: 6 },
261
- { name: 'email', type: 'email', label: 'Email', colSpan: 6 },
395
+ {
396
+ name: 'contacts',
397
+ type: 'text',
398
+ isArray: true,
399
+ label: 'Contacts',
400
+ addLabel: '+ Add Contact',
401
+ removeLabel: 'Remove',
402
+ minItems: 1,
403
+ maxItems: 5,
404
+ fields: [
405
+ { name: 'name', type: 'text', label: 'Name', colSpan: 6 },
406
+ { name: 'email', type: 'email', label: 'Email', colSpan: 6 },
407
+ ],
408
+ } satisfies ArrayFieldConfig,
262
409
  ],
263
410
  };
264
411
  ```
265
412
 
266
- ### Imperative Control via Ref
413
+ **Array helpers** available via `arrayRef`:
414
+
415
+ - `push(value)` — add item at end
416
+ - `remove(index)` — remove item
417
+ - `swap(a, b)` — swap two items
418
+ - `insert(index, value)` — insert at position
419
+ - `replace(index, value)` — replace item
420
+ - `move(from, to)` — move item
421
+
422
+ #### 07 · Ref Control
423
+
424
+ Access the form imperatively from parent components.
267
425
 
268
426
  ```tsx
269
427
  import { useRef } from 'react';
270
428
  import type { AxenFormRef } from '@axenstudio/axen-form';
271
429
 
272
- function MyForm() {
430
+ function MyPage() {
273
431
  const formRef = useRef<AxenFormRef>(null);
274
432
 
275
- const handleExternalSubmit = () => formRef.current?.submit();
276
- const handleReset = () => formRef.current?.resetForm();
277
-
278
433
  return (
279
434
  <>
280
435
  <AxenForm
@@ -283,15 +438,17 @@ function MyForm() {
283
438
  components={defaultComponentMap}
284
439
  onSubmit={handleSubmit}
285
440
  />
286
- <button onClick={handleExternalSubmit}>Submit from Outside</button>
287
- <button onClick={handleReset}>Reset</button>
441
+ <div>
442
+ <button onClick={() => formRef.current?.submit()}>Submit from Outside</button>
443
+ <button onClick={() => formRef.current?.resetForm()}>Reset</button>
444
+ <button onClick={() => console.log(formRef.current?.getValues())}>Inspect Values</button>
445
+ <button onClick={() => console.log(formRef.current?.isValid())}>Check Valid</button>
446
+ </div>
288
447
  </>
289
448
  );
290
449
  }
291
450
  ```
292
451
 
293
- **Available ref methods:**
294
-
295
452
  | Method | Return | Description |
296
453
  | -------------------- | ------------------------- | --------------------------------------- |
297
454
  | `submit()` | `void` | Trigger form submission |
@@ -301,45 +458,84 @@ function MyForm() {
301
458
  | `isValid()` | `boolean` | Check if form has no errors |
302
459
  | `isDirty()` | `boolean` | Check if values differ from initial |
303
460
 
304
- ### Payload Whitelist
461
+ #### 08 · Payload Mapping
305
462
 
306
- Control which fields are included in the submit payload:
463
+ Control which fields are sent to `onSubmit` — prevent hidden/irrelevant fields from leaking to your API.
307
464
 
308
465
  ```tsx
309
- // Static whitelist
466
+ // Static whitelist — only name and email are submitted
310
467
  <AxenForm
311
468
  config={config}
312
469
  components={defaultComponentMap}
313
470
  payloadFields={['name', 'email']}
314
471
  onSubmit={(values) => {
315
- // values only contains { name, email }
472
+ // values: { name, email } — nothing else
316
473
  }}
317
474
  />
318
475
 
319
- // Dynamic whitelist based on discriminator field
476
+ // Dynamic whitelist — different fields per accountType
320
477
  <AxenForm
321
478
  config={config}
322
479
  components={defaultComponentMap}
323
480
  payloadFields={{
324
- individual: ['firstName', 'lastName', 'ssn'],
325
- company: ['companyName', 'taxId'],
481
+ personal: ['firstName', 'lastName', 'ssn'],
482
+ business: ['companyName', 'taxId', 'industry'],
326
483
  }}
327
484
  payloadDiscriminator="accountType"
328
485
  onSubmit={handleSubmit}
329
486
  />
330
487
  ```
331
488
 
332
- ### Headless Usage with `useAxenForm`
489
+ ### EXPERT
490
+
491
+ #### 09 · Form Context
492
+
493
+ Access form state from deeply nested child components using context hooks.
494
+
495
+ ```tsx
496
+ import { useFormContext, useField } from '@axenstudio/axen-form';
497
+
498
+ function StatusBadge() {
499
+ const { store } = useFormContext();
500
+ const state = store.getState();
333
501
 
334
- For full control over rendering, use the headless hook:
502
+ return (
503
+ <span>
504
+ {state.dirty ? '● Unsaved changes' : '✓ Saved'}
505
+ {' | '}
506
+ Errors: {Object.keys(state.errors).length}
507
+ </span>
508
+ );
509
+ }
510
+
511
+ function InlineFieldDisplay({ name }: { name: string }) {
512
+ const { value, error, touched } = useField(name);
513
+ return (
514
+ <div>
515
+ <strong>{name}:</strong> {String(value)}
516
+ {touched && error && <span style={{ color: 'red' }}> — {error}</span>}
517
+ </div>
518
+ );
519
+ }
520
+
521
+ // Nest these inside AxenForm's children
522
+ <AxenForm config={config} components={defaultComponentMap} onSubmit={handleSubmit}>
523
+ <StatusBadge />
524
+ <InlineFieldDisplay name="email" />
525
+ </AxenForm>;
526
+ ```
527
+
528
+ **Headless usage with `useAxenForm`:**
335
529
 
336
530
  ```tsx
337
- import { useAxenForm, useField } from '@axenstudio/axen-form';
531
+ import { useAxenForm } from '@axenstudio/axen-form';
338
532
 
339
- function CustomForm() {
533
+ function FullyCustomForm() {
340
534
  const { store, formApi } = useAxenForm({
341
- initialValues: { name: '' },
342
- fields: [{ name: 'name', type: 'text', label: 'Name', required: true }],
535
+ config: {
536
+ initialValues: { name: '' },
537
+ fields: [{ name: 'name', type: 'text', required: true }],
538
+ },
343
539
  onSubmit: async (values) => {
344
540
  /* ... */
345
541
  },
@@ -360,78 +556,322 @@ function CustomForm() {
360
556
  }
361
557
  ```
362
558
 
363
- ### Custom Field Components
559
+ #### 10 · Custom Component
364
560
 
365
- Override any built-in field by providing a custom component:
561
+ Override any built-in field with your own component.
366
562
 
367
563
  ```tsx
368
564
  import type { FieldComponentProps } from '@axenstudio/axen-form';
369
565
 
370
- function MyCustomSelect({ name, value, onChange, options, label, error, helperText }: FieldComponentProps) {
566
+ // Custom star-rating component
567
+ function StarRating({ name, value, onChange, label, error, helperText }: FieldComponentProps) {
568
+ const stars = [1, 2, 3, 4, 5];
569
+ const current = Number(value) || 0;
570
+
371
571
  return (
372
572
  <div>
373
573
  <label>{label}</label>
374
- {/* Your custom select UI */}
375
- {error && <span className="error">{helperText}</span>}
574
+ <div>
575
+ {stars.map((star) => (
576
+ <button
577
+ key={star}
578
+ type="button"
579
+ onClick={() => {
580
+ const syntheticEvent = { target: { name, value: star } };
581
+ onChange(syntheticEvent as any);
582
+ }}
583
+ style={{
584
+ color: star <= current ? 'gold' : '#ccc',
585
+ fontSize: '1.5rem',
586
+ background: 'none',
587
+ border: 'none',
588
+ }}
589
+ >
590
+ ★
591
+ </button>
592
+ ))}
593
+ </div>
594
+ {error && <span style={{ color: 'red', fontSize: '0.75rem' }}>{helperText}</span>}
376
595
  </div>
377
596
  );
378
597
  }
379
598
 
380
599
  // Per-field override
381
600
  const config = {
382
- initialValues: { country: '' },
383
- fields: [
384
- { name: 'country', type: 'select', label: 'Country', component: MyCustomSelect, options: [...] },
385
- ],
601
+ initialValues: { rating: 0 },
602
+ fields: [{ name: 'rating', type: 'text', label: 'Your Rating', component: StarRating }],
386
603
  };
387
604
 
388
605
  // Or type-level override via components prop
389
606
  <AxenForm
390
607
  config={config}
391
- components={{ ...defaultComponentMap, select: MyCustomSelect }}
608
+ components={{ ...defaultComponentMap, 'star-rating': StarRating }}
392
609
  onSubmit={handleSubmit}
393
- />
610
+ />;
611
+ ```
612
+
613
+ ---
614
+
615
+ ## Built-in Field Types (19)
616
+
617
+ Every field renders enterprise-grade out of the box — with icons, placeholders, themed styling, and WCAG-compliant focus rings.
618
+
619
+ ### Text Inputs
620
+
621
+ | Type | Component | Icon | Default Placeholder | Description |
622
+ | ---------- | --------------- | ----------- | ---------------------- | ---------------------------------- |
623
+ | `text` | `TextField` | ✏️ Pencil | `Enter text...` | Standard text input |
624
+ | `email` | `EmailField` | ✉️ Envelope | `user@example.com` | Email input with `type="email"` |
625
+ | `password` | `PasswordField` | 🔒 Lock | `Enter password...` | Password with show/hide toggle |
626
+ | `phone` | `PhoneField` | 📞 Phone | `+1 (555) 000-0000` | Phone input with `type="tel"` |
627
+ | `textarea` | `TextareaField` | — | `Enter description...` | Multi-line text, configurable rows |
628
+
629
+ ### Numeric Inputs
630
+
631
+ | Type | Component | Icon | Default Placeholder | Description |
632
+ | ---------- | --------------- | ---------- | ------------------- | ----------------------------------------------------- |
633
+ | `number` | `NumberField` | `#` Hash | `0` | Locale-aware thousand separator + cursor preservation |
634
+ | `currency` | `CurrencyField` | `$` Dollar | `0.00` | Intl currency formatting with prefix/suffix |
635
+ | `slider` | `SliderField` | — | — | Range slider with value display |
636
+
637
+ **NumberField** preserves cursor position after reformatting. Set `locale` (e.g., `'id-ID'` for dot-thousands, comma-decimal) and `min`/`max`/`step` for constraints.
638
+
639
+ **CurrencyField** uses `Intl.NumberFormat` with `currency` (e.g., `'USD'`, `'IDR'`) and `locale` for automatic symbol and formatting.
640
+
641
+ ### Date & Time
642
+
643
+ | Type | Component | Icon | Default Placeholder | Description |
644
+ | ---------- | --------------- | ----------- | ---------------------- | ------------------------------------------------------------- |
645
+ | `date` | `DateField` | 📅 Calendar | `Select date` | Custom date picker — displays locale, stores ISO `YYYY-MM-DD` |
646
+ | `time` | `TimeField` | 🕐 Clock | `Select time` | Custom time picker — 12h/24h, stores ISO `HH:mm` |
647
+ | `datetime` | `DateTimeField` | 📅 Calendar | `Select date and time` | Combined date+time, stores ISO `YYYY-MM-DDTHH:mm` |
648
+
649
+ All date/time fields store ISO strings internally and display locale-formatted values. Set `ampm: true` for 12-hour mode, `showSeconds: true` to include seconds.
650
+
651
+ ### Selection
652
+
653
+ | Type | Component | Description |
654
+ | ---------- | --------------- | -------------------------- |
655
+ | `select` | `SelectField` | Native `<select>` dropdown |
656
+ | `checkbox` | `CheckboxField` | Styled checkbox with label |
657
+ | `radio` | `RadioField` | Radio group from `options` |
658
+ | `switch` | `SwitchField` | Toggle switch (on/off) |
659
+
660
+ ### Autocomplete
661
+
662
+ | Type | Component | Icon | Default Placeholder | Description |
663
+ | ---------------------- | -------------------------- | --------- | ------------------- | ---------------------------------- |
664
+ | `autocomplete` | `AutocompleteField` | 🔍 Search | `Search...` | Server-side search with debounce |
665
+ | `autocomplete-multi` | `AutocompleteMultiField` | — | `Search...` | Multi-select with chip/tag display |
666
+ | `autocomplete-predict` | `AutocompletePredictField` | 🔍 Search | `Search...` | Ghost text prediction + Tab accept |
667
+
668
+ **AutocompleteField** accepts `fetchOptions: (query, signal?) => Promise<Option[]>` for server-side search with built-in debounce and keyboard navigation (↑↓ Enter Esc).
669
+
670
+ **AutocompletePredictField** shows ghost text predictions that users accept with Tab — similar to AI autocomplete in code editors.
671
+
672
+ ### Miscellaneous
673
+
674
+ | Type | Component | Description |
675
+ | ------- | ------------ | ------------------------ |
676
+ | `color` | `ColorField` | Color swatch + hex input |
677
+
678
+ ---
679
+
680
+ ## Enterprise Features
681
+
682
+ These features are active by default without extra configuration.
683
+
684
+ ### Built-in Icon System
685
+
686
+ Every field that accepts text input has a built-in SVG icon rendered inside the input (left side). Icons are inline SVGs (~200 bytes each), zero-dependency, tree-shakeable.
687
+
688
+ ```tsx
689
+ // Default icon (auto per field type)
690
+ { name: 'email', type: 'email', label: 'Email' }
691
+
692
+ // Custom icon override
693
+ { name: 'search', type: 'text', label: 'Search', icon: <MySearchIcon /> }
694
+
695
+ // Disable icon
696
+ { name: 'plain', type: 'text', label: 'Plain', icon: false }
394
697
  ```
395
698
 
699
+ **Icon map:**
700
+
701
+ | Field Type | Icon |
702
+ | ---------------------- | ---------------- |
703
+ | `text` | Pencil / Edit |
704
+ | `email` | Envelope |
705
+ | `password` | Lock |
706
+ | `phone` | Phone |
707
+ | `number` | Hash `#` |
708
+ | `currency` | Dollar `$` |
709
+ | `date`, `datetime` | Calendar |
710
+ | `time` | Clock |
711
+ | `autocomplete` | Magnifying glass |
712
+ | `autocomplete-predict` | Magnifying glass |
713
+
714
+ Fields without icons: `textarea`, `slider`, `select`, `checkbox`, `radio`, `switch`, `autocomplete-multi`, `color`.
715
+
716
+ ### Default Placeholders
717
+
718
+ Fields without an explicit `placeholder` receive a sensible default based on their type (`Enter text...`, `user@example.com`, `Select date`, etc.). Override with a custom string or set `placeholder: ''` to clear.
719
+
720
+ ### Field Groups / Sections
721
+
722
+ Group related fields under a visual section header. Groups are purely visual — they don't create nested data structures.
723
+
724
+ ```tsx
725
+ import type { FieldGroupConfig } from '@axenstudio/axen-form';
726
+
727
+ const config = {
728
+ initialValues: { firstName: '', lastName: '', email: '', phone: '', street: '', city: '' },
729
+ fields: [
730
+ {
731
+ group: 'Personal Information',
732
+ fields: [
733
+ { name: 'firstName', type: 'text', label: 'First Name', colSpan: 6 },
734
+ { name: 'lastName', type: 'text', label: 'Last Name', colSpan: 6 },
735
+ { name: 'email', type: 'email', label: 'Email' },
736
+ { name: 'phone', type: 'phone', label: 'Phone' },
737
+ ],
738
+ } satisfies FieldGroupConfig,
739
+ {
740
+ group: 'Address',
741
+ fields: [
742
+ { name: 'street', type: 'text', label: 'Street' },
743
+ { name: 'city', type: 'text', label: 'City', colSpan: 6 },
744
+ ],
745
+ } satisfies FieldGroupConfig,
746
+ ],
747
+ };
748
+ ```
749
+
750
+ Group headers render as bold, themed labels with a subtle bottom border, spanning the full grid width.
751
+
752
+ ### Theme System
753
+
754
+ axen-form ships with 3 built-in themes applied via CSS custom properties. The `default` theme is auto-applied when no `theme` prop is set.
755
+
756
+ ```tsx
757
+ // Default blue theme (auto-applied)
758
+ <AxenForm config={config} components={defaultComponentMap} onSubmit={handleSubmit} />
759
+
760
+ // Subtle gray theme
761
+ <AxenForm config={config} components={defaultComponentMap} theme="subtle" onSubmit={handleSubmit} />
762
+
763
+ // Green theme
764
+ <AxenForm config={config} components={defaultComponentMap} theme="green" onSubmit={handleSubmit} />
765
+ ```
766
+
767
+ **Built-in presets:**
768
+
769
+ | Theme | Primary | Input Background | Label Color |
770
+ | --------- | ----------------- | ---------------- | ----------- |
771
+ | `default` | `#1976d2` (Blue) | `#f0f7ff` | `#1565c0` |
772
+ | `subtle` | `#546e7a` (Gray) | `#f5f5f5` | `#37474f` |
773
+ | `green` | `#2e7d32` (Green) | `#f1f8e9` | `#1b5e20` |
774
+
775
+ **Custom themes:** Create your own by adding a CSS rule targeting `[data-axen-theme='yourTheme']`:
776
+
777
+ ```css
778
+ [data-axen-theme='coral'] {
779
+ --axen-color-primary: #e74c3c;
780
+ --axen-color-primary-hover: #c0392b;
781
+ --axen-color-primary-light: #fde8e8;
782
+ --axen-color-input-bg: #fef5f5;
783
+ --axen-color-label: #c0392b;
784
+ }
785
+ ```
786
+
787
+ Then: `<AxenForm theme="coral" ... />`
788
+
396
789
  ---
397
790
 
398
- ## Layout Components
791
+ ## Layout System
792
+
793
+ ### Grid Layout (12-column)
794
+
795
+ AxenForm renders fields inside a 12-column CSS Grid. Each field occupies columns based on its `colSpan` property. Default is full-width (`colSpan: 12`).
399
796
 
400
- Built-in layout components with MUI-like API, powered by CSS Grid:
797
+ ```
798
+ ┌────────────────────────────────────────────────┐
799
+ │ colSpan: 12 (full width) │
800
+ ├────────────────────────┬───────────────────────┤
801
+ │ colSpan: 6 (half) │ colSpan: 6 (half) │
802
+ ├────────────┬───────────┼───────────────────────┤
803
+ │ colSpan: 4 │ colSpan: 4│ colSpan: 4 │
804
+ └────────────┴───────────┴───────────────────────┘
805
+ ```
806
+
807
+ The grid maxes out at `900px` width and auto-centers — no external container needed.
401
808
 
402
- ### Grid
809
+ ### Responsive colSpan
403
810
 
404
- 12-column responsive grid container:
811
+ Use an object to set different spans per breakpoint. Values cascade upward (xs → sm → md → lg → xl).
812
+
813
+ ```tsx
814
+ {
815
+ name: 'city',
816
+ type: 'text',
817
+ label: 'City',
818
+ colSpan: {
819
+ xs: 12, // Full width on mobile (<600px)
820
+ sm: 6, // Half width on tablet (≥600px)
821
+ md: 4, // Third on desktop (≥900px)
822
+ },
823
+ }
824
+ ```
825
+
826
+ **Breakpoints:**
827
+
828
+ | Key | Min Width | Typical Device |
829
+ | ---- | --------- | ---------------- |
830
+ | `xs` | 0 | Mobile |
831
+ | `sm` | 600px | Tablet portrait |
832
+ | `md` | 900px | Tablet landscape |
833
+ | `lg` | 1200px | Desktop |
834
+ | `xl` | 1536px | Large desktop |
835
+
836
+ Or use a number for all breakpoints: `colSpan: 6` = half-width on all screens.
837
+
838
+ ### Layout Components
839
+
840
+ In addition to config-driven form layout, axen-form exports standalone layout components with a MUI-like API, powered by CSS Grid/Flexbox.
841
+
842
+ #### Grid
843
+
844
+ 12-column responsive grid — independent layout component.
405
845
 
406
846
  ```tsx
407
847
  import { Grid } from '@axenstudio/axen-form';
408
848
 
409
849
  <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>
850
+ <Grid size={{ xs: 12, md: 6 }}>Left half</Grid>
851
+ <Grid size={{ xs: 12, md: 6 }}>Right half</Grid>
416
852
  </Grid>;
417
853
  ```
418
854
 
419
- ### Box
855
+ **Props:** `container`, `columns` (default 12), `spacing`, `rowSpacing`, `columnSpacing`, `size`, `xs`/`sm`/`md`/`lg`/`xl`, `offset`.
856
+
857
+ #### Box
420
858
 
421
- Generic flex/block container:
859
+ Generic flex/block container with spacing shortcuts.
422
860
 
423
861
  ```tsx
424
862
  import { Box } from '@axenstudio/axen-form';
425
863
 
426
- <Box display="flex" gap="16px" p="8px">
864
+ <Box display="flex" gap="16px" p={2} justifyContent="space-between">
427
865
  <span>Left</span>
428
866
  <span>Right</span>
429
867
  </Box>;
430
868
  ```
431
869
 
432
- ### Stack
870
+ **Props:** `display`, `flexDirection`, `justifyContent`, `alignItems`, `flexWrap`, `gap`, `p`/`px`/`py`, `m`/`mx`/`my`.
433
871
 
434
- Vertical/horizontal stack with gap:
872
+ #### Stack
873
+
874
+ Vertical/horizontal stack with consistent gap.
435
875
 
436
876
  ```tsx
437
877
  import { Stack } from '@axenstudio/axen-form';
@@ -442,9 +882,11 @@ import { Stack } from '@axenstudio/axen-form';
442
882
  </Stack>;
443
883
  ```
444
884
 
445
- ### Divider
885
+ **Props:** `direction` (row/column), `spacing`, `alignItems`, `justifyContent`, `divider`.
886
+
887
+ #### Divider
446
888
 
447
- Horizontal/vertical divider:
889
+ Horizontal or vertical separator line.
448
890
 
449
891
  ```tsx
450
892
  import { Divider } from '@axenstudio/axen-form';
@@ -456,83 +898,89 @@ import { Divider } from '@axenstudio/axen-form';
456
898
  </Stack>;
457
899
  ```
458
900
 
459
- ### Spacer
901
+ #### Spacer
460
902
 
461
- Invisible spacing element — creates horizontal, vertical, or flex-basis gaps. Uses the same 8px spacing unit as Box/Grid/Stack.
903
+ Invisible spacing element. Uses 8px unit multiplier.
462
904
 
463
905
  ```tsx
464
906
  import { Spacer } from '@axenstudio/axen-form';
465
907
 
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>
908
+ // Horizontal 16px gap
909
+ <Spacer x={2} />
479
910
 
480
911
  // 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} />
912
+ <Stack direction="row"><Logo /><Spacer grow /><NavLinks /></Stack>
489
913
  ```
490
914
 
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 |
915
+ | Prop | Type | Description |
916
+ | ------- | --------- | ------------------------------------ |
917
+ | `x` | `number` | Horizontal spacing (width = x × 8px) |
918
+ | `y` | `number` | Vertical spacing (height = y × 8px) |
919
+ | `basis` | `number` | Flex basis (basis × 8px) |
920
+ | `grow` | `boolean` | Grow to fill remaining space |
497
921
 
498
922
  ---
499
923
 
500
- ## Theming with CSS Custom Properties
924
+ ## Core Concepts
501
925
 
502
- axen-form uses CSS Custom Properties (design tokens) for theming. Import the default tokens and override as needed:
926
+ ### Config-Driven Forms
503
927
 
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
- }
928
+ Define your entire form with a single configuration object — fields, layout, validation, and behavior:
929
+
930
+ ```typescript
931
+ import type { FormConfig } from '@axenstudio/axen-form';
932
+
933
+ const config: FormConfig = {
934
+ initialValues: { name: '', email: '', age: 0 },
935
+ fields: [
936
+ { name: 'name', type: 'text', label: 'Name', required: true },
937
+ {
938
+ name: 'email',
939
+ type: 'email',
940
+ label: 'Email',
941
+ required: true,
942
+ requiredMessage: 'Email wajib diisi',
943
+ },
944
+ { name: 'age', type: 'number', label: 'Age', min: 0, max: 150, locale: 'en-US' },
945
+ ],
946
+ };
517
947
  ```
518
948
 
519
- **Key token categories:**
949
+ ### Reactive Form State
520
950
 
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` |
951
+ axen-form uses a built-in reactive store (no Formik dependency). State is managed via `useSyncExternalStore` — only the fields that change re-render.
529
952
 
530
- **Built-in A11y support:**
953
+ - **Field-level subscription:** components subscribe to specific field paths
954
+ - **Dirty detection:** deep comparison against initial values
955
+ - **Sync validation:** runs on change/blur (configurable)
956
+
957
+ ### Validation Adapters
958
+
959
+ Plug in any validation library:
960
+
961
+ | Adapter | Import Path | Library |
962
+ | ----------------- | ------------------------------------ | ------- |
963
+ | `yupAdapter` | `@axenstudio/axen-form/adapters/yup` | Yup |
964
+ | `zodAdapter` | `@axenstudio/axen-form/adapters/zod` | Zod |
965
+ | `simpleValidator` | Built-in (default) | None |
966
+
967
+ Both adapters auto-generate schemas from `FieldConfig` (required, min, max, type-aware schema). You can also provide custom `validation` on individual fields.
968
+
969
+ ---
970
+
971
+ ## Advanced Features
972
+
973
+ ### Payload Whitelist
974
+
975
+ Control which fields are sent to `onSubmit`. See [Scenario 08](#08--payload-mapping).
976
+
977
+ ### Headless Usage
531
978
 
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
979
+ Use `useAxenForm` for full rendering control. See [Scenario 09](#09--form-context).
980
+
981
+ ### Custom Field Components
982
+
983
+ Override any built-in field. See [Scenario 10](#10--custom-component).
536
984
 
537
985
  ---
538
986
 
@@ -549,62 +997,83 @@ Main config-driven form component.
549
997
  | `components` | `FieldTypeComponentMap` | `{}` | Component map (use `defaultComponentMap`) |
550
998
  | `validationAdapter` | `ValidationAdapter` | `simpleValidator` | Validation library adapter |
551
999
  | `ref` | `Ref<AxenFormRef>` | — | Imperative handle |
1000
+ | `theme` | `'default' \| 'subtle' \| string` | `'default'` | Theme preset or custom name |
552
1001
  | `columns` | `number` | `12` | Grid columns |
553
- | `gap` | `string \| number` | — | Grid gap |
1002
+ | `gap` | `string \| number` | `'1rem'` | Grid gap |
554
1003
  | `disabled` | `boolean` | `false` | Disable all fields |
555
1004
  | `className` | `string` | — | Additional CSS class |
556
1005
  | `payloadFields` | `string[] \| Record<string, string[]>` | — | Payload whitelist |
557
1006
  | `payloadDiscriminator` | `string` | — | Discriminator field for dynamic payload |
558
1007
  | `validateOnChange` | `boolean` | `true` | Validate on field change |
559
1008
  | `validateOnBlur` | `boolean` | `true` | Validate on field blur |
1009
+ | `children` | `ReactNode` | — | Extra content inside form (buttons, etc.) |
560
1010
 
561
1011
  ### `FieldConfig`
562
1012
 
563
1013
  Configuration for a single field.
564
1014
 
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 |
1015
+ | Property | Type | Description |
1016
+ | ------------------ | --------------------------------------- | -------------------------------------------------- |
1017
+ | `name` | `string` | Field name (supports nested paths: `address.city`) |
1018
+ | `type` | `BuiltInFieldType \| string` | Field type identifier |
1019
+ | `label` | `string` | Field label |
1020
+ | `placeholder` | `string` | Input placeholder (auto-filled if not set) |
1021
+ | `required` | `boolean` | Mark as required |
1022
+ | `requiredMessage` | `string` | Custom required error message |
1023
+ | `disabled` | `boolean` | Disable this field |
1024
+ | `readOnly` | `boolean` | Read-only mode |
1025
+ | `hidden` | `boolean \| (values) => boolean` | Conditional hiding |
1026
+ | `component` | `ComponentType` | Override component for this field |
1027
+ | `options` | `FieldOption[]` | Options for select/radio/autocomplete |
1028
+ | `icon` | `ReactNode \| false` | Custom icon, or `false` to disable |
1029
+ | `colSpan` | `number \| { xs?, sm?, md?, lg?, xl? }` | Grid column span (default: 12) |
1030
+ | `helperText` | `string` | Helper text below field |
1031
+ | `min`, `max` | `number` | Numeric/date constraints |
1032
+ | `step` | `number` | Numeric step value |
1033
+ | `prefix`, `suffix` | `string` | Display prefix/suffix (currency) |
1034
+ | `locale` | `string` | Locale for number/date formatting |
1035
+ | `currency` | `string` | Currency code (e.g., `'USD'`, `'IDR'`) |
1036
+ | `rows` | `number` | Textarea rows |
1037
+ | `ampm` | `boolean` | 12-hour mode for time/datetime |
1038
+ | `showSeconds` | `boolean` | Show seconds in time picker |
1039
+ | `fetchOptions` | `(query, signal?) => Promise<Option[]>` | Async option fetcher (autocomplete) |
1040
+ | `optionLabel` | `string` | Template: `'{name} - {email}'` |
1041
+ | `multiple` | `boolean` | Multi-select for autocomplete |
1042
+ | `fieldProps` | `Record<string, unknown>` | Extra props forwarded to component |
1043
+ | `className` | `string` | Additional CSS class for field wrapper |
1044
+
1045
+ ### `FieldGroupConfig`
1046
+
1047
+ | Property | Type | Description |
1048
+ | ----------- | --------------------------------------- | --------------------------- |
1049
+ | `group` | `string` | Section header label |
1050
+ | `fields` | `FieldConfig[]` | Fields within the group |
1051
+ | `columns` | `number` | Sub-grid columns (optional) |
1052
+ | `colSpan` | `number \| { xs?, sm?, md?, lg?, xl? }` | Group wrapper column span |
1053
+ | `className` | `string` | Additional CSS class |
586
1054
 
587
1055
  ### Hooks
588
1056
 
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 |
1057
+ | Hook | Signature | Description |
1058
+ | ---------------- | ------------------------------------------------------------------------- | ------------------------------- |
1059
+ | `useAxenForm` | `(opts) => { store, formApi }` | Create a headless form instance |
1060
+ | `useField` | `(name) => { value, error, touched, onChange, onBlur }` | Subscribe to a single field |
1061
+ | `useFieldArray` | `(name) => { fields, push, remove, swap, insert, replace, move, length }` | Array field operations |
1062
+ | `useFormContext` | `() => { store, fields, components, disabled }` | Access form context |
595
1063
 
596
1064
  ### Utilities
597
1065
 
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 |
1066
+ | Utility | Description |
1067
+ | ----------------------------------- | -------------------------------------- |
1068
+ | `mapPayload(values, fields, disc)` | Apply payload whitelist to form values |
1069
+ | `pickFields(values, names)` | Pick specific fields from values |
1070
+ | `getStrippedFields(values, names)` | Get fields that would be stripped |
1071
+ | `get(obj, path, default?)` | Deep get via dot path |
1072
+ | `set(obj, path, value)` | Immutable deep set |
1073
+ | `formatNumber(value, opts)` | Format number with locale |
1074
+ | `formatDateLocale(isoDate, locale)` | Format ISO date for display |
1075
+ | `formatTimeLocale(isoTime, ...)` | Format ISO time for display |
1076
+ | `isFieldGroup(item)` | Type guard for FieldGroupConfig |
608
1077
 
609
1078
  ---
610
1079
 
@@ -613,7 +1082,7 @@ Configuration for a single field.
613
1082
  ```
614
1083
  src/
615
1084
  ├── core/ # Framework-agnostic form engine
616
- │ ├── formStore.ts # Reactive state (useSyncExternalStore compatible)
1085
+ │ ├── formStore.ts # Reactive state (useSyncExternalStore)
617
1086
  │ ├── validation.ts # Adapter interface + simpleValidator
618
1087
  │ ├── pathUtils.ts # Nested path get/set/toArray
619
1088
  │ ├── numberUtils.ts # Locale-aware number formatting
@@ -622,24 +1091,42 @@ src/
622
1091
  │
623
1092
  ├── react/ # React 19+ bindings
624
1093
  │ ├── AxenForm.tsx # Main config-driven renderer
625
- │ ├── AxenField.tsx # Field wrapper component
1094
+ │ ├── AxenField.tsx # Standalone field wrapper
626
1095
  │ ├── AxenArrayField.tsx # Array field renderer
627
1096
  │ ├── useAxenForm.ts # Headless form hook
628
1097
  │ ├── useField.ts # Single field subscription
629
1098
  │ ├── useFieldArray.ts # Array field operations
630
- │ └── FormContext.ts # React context provider
1099
+ │ └── FormContext.ts # React context
631
1100
  │
632
- ├── fields/ # 20 built-in field components
633
- │ ├── defaultComponentMap.ts
634
- │ ├── fieldUtils.ts # Shared utilities (cx, ariaProps)
635
- │ ├── fieldBase.module.css # Shared field styles
1101
+ ├── fields/ # 19 built-in field components
1102
+ │ ├── icons.tsx # Inline SVG icons
1103
+ │ ├── iconUtils.ts # Icon resolution logic
1104
+ │ ├── defaultPlaceholders.ts # Default placeholder map
1105
+ │ ├── defaultComponentMap.ts # Type → Component registry
1106
+ │ ├── fieldUtils.ts # Shared utilities (cx, aria)
1107
+ │ ├── fieldBase.module.css # Shared field styles
636
1108
  │ ├── TextField/
637
1109
  │ ├── EmailField/
638
- │ ├── ... # 18 more field directories
1110
+ │ ├── PasswordField/ # Show/hide toggle
1111
+ │ ├── PhoneField/
1112
+ │ ├── TextareaField/
1113
+ │ ├── NumberField/ # Cursor-preserving formatting
1114
+ │ ├── CurrencyField/ # Intl currency
1115
+ │ ├── SliderField/
1116
+ │ ├── DateField/ # Custom DatePicker
1117
+ │ ├── TimeField/ # Custom TimePicker
1118
+ │ ├── DateTimeField/
1119
+ │ ├── SelectField/
1120
+ │ ├── CheckboxField/
1121
+ │ ├── RadioField/
1122
+ │ ├── SwitchField/
1123
+ │ ├── AutocompleteField/ # Server search + debounce
1124
+ │ ├── AutocompleteMultiField/ # Multi-select chips
1125
+ │ ├── AutocompletePredictField/ # Ghost text
639
1126
  │ └── ColorField/
640
1127
  │
641
- ├── layout/ # Grid, Box, Stack, Divider
642
- ├── tokens/ # CSS Custom Properties + reset
1128
+ ├── layout/ # Grid, Box, Stack, Divider, Spacer
1129
+ ├── tokens/ # CSS tokens, themes, reset
643
1130
  ├── adapters/ # yupAdapter, zodAdapter
644
1131
  ├── types.ts # All TypeScript definitions
645
1132
  └── index.ts # Public API barrel
@@ -647,6 +1134,32 @@ src/
647
1134
 
648
1135
  ---
649
1136
 
1137
+ ## Theming with CSS Custom Properties
1138
+
1139
+ axen-form uses CSS Custom Properties (design tokens) for fine-grained theming.
1140
+
1141
+ **Key token categories:**
1142
+
1143
+ | Category | Example Tokens |
1144
+ | ---------- | --------------------------------------------------------------------- |
1145
+ | Colors | `--axen-color-primary`, `--axen-color-error`, `--axen-color-input-bg` |
1146
+ | Typography | `--axen-font-family`, `--axen-font-size-sm`/`md`/`lg` |
1147
+ | Spacing | `--axen-space-xs`/`sm`/`md`/`lg`/`xl` |
1148
+ | Borders | `--axen-color-border`, `--axen-border-radius` |
1149
+ | Focus | `--axen-shadow-focus`, `--axen-color-border-focus` |
1150
+ | Fields | `--axen-field-height`, `--axen-field-min-height` (44px touch target) |
1151
+
1152
+ **Built-in A11y support:**
1153
+
1154
+ - `@media (prefers-color-scheme: dark)` — dark mode tokens
1155
+ - `@media (prefers-contrast: more)` — high contrast overrides
1156
+ - `@media (prefers-reduced-motion: reduce)` — reduced motion
1157
+ - `focus-visible` rings for keyboard navigation
1158
+ - `aria-invalid`, `aria-describedby` on all fields
1159
+ - Minimum 44px touch targets (WCAG 2.1 AA)
1160
+
1161
+ ---
1162
+
650
1163
  ## Browser Support
651
1164
 
652
1165
  | Browser | Version |