@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.
- package/README.md +783 -270
- package/dist/core/dateUtils.cjs +7 -3
- package/dist/core/dateUtils.d.ts +1 -1
- package/dist/core/dateUtils.d.ts.map +1 -1
- package/dist/core/dateUtils.js +7 -3
- package/dist/core/validation.cjs +4 -1
- package/dist/core/validation.d.ts.map +1 -1
- package/dist/core/validation.js +4 -1
- package/dist/fields/AutocompleteField/AutocompleteField.cjs +32 -32
- package/dist/fields/AutocompleteField/AutocompleteField.js +32 -32
- package/dist/fields/AutocompleteField/AutocompleteField.module.css.cjs +34 -13
- package/dist/fields/AutocompleteField/AutocompleteField.module.css.js +34 -13
- package/dist/fields/AutocompleteMultiField/AutocompleteMultiField.cjs +90 -88
- package/dist/fields/AutocompleteMultiField/AutocompleteMultiField.d.ts.map +1 -1
- package/dist/fields/AutocompleteMultiField/AutocompleteMultiField.js +90 -88
- package/dist/fields/AutocompletePredictField/AutocompletePredictField.cjs +81 -64
- package/dist/fields/AutocompletePredictField/AutocompletePredictField.d.ts.map +1 -1
- package/dist/fields/AutocompletePredictField/AutocompletePredictField.js +81 -64
- package/dist/fields/CheckboxField/CheckboxField.module.css.cjs +12 -0
- package/dist/fields/CheckboxField/CheckboxField.module.css.js +12 -0
- package/dist/fields/ColorField/ColorField.module.css.cjs +12 -0
- package/dist/fields/ColorField/ColorField.module.css.js +12 -0
- package/dist/fields/DateField/DateField.cjs +56 -4
- package/dist/fields/DateField/DateField.d.ts +1 -1
- package/dist/fields/DateField/DateField.d.ts.map +1 -1
- package/dist/fields/DateField/DateField.js +56 -4
- package/dist/fields/DateField/DatePicker.cjs +194 -0
- package/dist/fields/DateField/DatePicker.d.ts +11 -0
- package/dist/fields/DateField/DatePicker.d.ts.map +1 -0
- package/dist/fields/DateField/DatePicker.js +194 -0
- package/dist/fields/DateField/DatePicker.module.css.cjs +62 -0
- package/dist/fields/DateField/DatePicker.module.css.js +62 -0
- package/dist/fields/DateTimeField/DateTimeField.cjs +126 -8
- package/dist/fields/DateTimeField/DateTimeField.d.ts +2 -1
- package/dist/fields/DateTimeField/DateTimeField.d.ts.map +1 -1
- package/dist/fields/DateTimeField/DateTimeField.js +126 -8
- package/dist/fields/PasswordField/PasswordField.module.css.cjs +12 -0
- package/dist/fields/PasswordField/PasswordField.module.css.js +12 -0
- package/dist/fields/RadioField/RadioField.module.css.cjs +12 -0
- package/dist/fields/RadioField/RadioField.module.css.js +12 -0
- package/dist/fields/SliderField/SliderField.module.css.cjs +15 -3
- package/dist/fields/SliderField/SliderField.module.css.js +15 -3
- package/dist/fields/SwitchField/SwitchField.module.css.cjs +12 -0
- package/dist/fields/SwitchField/SwitchField.module.css.js +12 -0
- package/dist/fields/TimeField/TimeField.cjs +60 -5
- package/dist/fields/TimeField/TimeField.d.ts +2 -1
- package/dist/fields/TimeField/TimeField.d.ts.map +1 -1
- package/dist/fields/TimeField/TimeField.js +60 -5
- package/dist/fields/TimeField/TimePicker.cjs +182 -0
- package/dist/fields/TimeField/TimePicker.d.ts +11 -0
- package/dist/fields/TimeField/TimePicker.d.ts.map +1 -0
- package/dist/fields/TimeField/TimePicker.js +182 -0
- package/dist/fields/TimeField/TimePicker.module.css.cjs +50 -0
- package/dist/fields/TimeField/TimePicker.module.css.js +50 -0
- package/dist/fields/defaultPlaceholders.cjs +1 -0
- package/dist/fields/defaultPlaceholders.d.ts.map +1 -1
- package/dist/fields/defaultPlaceholders.js +1 -0
- package/dist/fields/fieldBase.module.css.cjs +366 -18
- package/dist/fields/fieldBase.module.css.js +366 -18
- package/dist/fields/icons.cjs +2 -0
- package/dist/fields/icons.d.ts +4 -0
- package/dist/fields/icons.d.ts.map +1 -1
- package/dist/fields/icons.js +2 -0
- package/dist/index.cjs +230 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +230 -3
- package/dist/layout/Box/Box.module.css.cjs +12 -0
- package/dist/layout/Box/Box.module.css.js +12 -0
- package/dist/layout/Divider/Divider.module.css.cjs +12 -0
- package/dist/layout/Divider/Divider.module.css.js +12 -0
- package/dist/layout/Grid/Grid.module.css.cjs +12 -0
- package/dist/layout/Grid/Grid.module.css.js +12 -0
- package/dist/layout/Stack/Stack.module.css.cjs +12 -0
- package/dist/layout/Stack/Stack.module.css.js +12 -0
- package/dist/react/AxenForm.cjs +228 -3
- package/dist/react/AxenForm.js +228 -3
- package/dist/react/fieldGroup.module.css.cjs +12 -0
- package/dist/react/fieldGroup.module.css.js +12 -0
- package/dist/react/gridCell.module.css.cjs +12 -0
- package/dist/react/gridCell.module.css.js +12 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -9
- package/dist/fields/AutocompleteField/AutocompleteField.module.css +0 -168
- package/dist/fields/CheckboxField/CheckboxField.module.css +0 -29
- package/dist/fields/ColorField/ColorField.module.css +0 -33
- package/dist/fields/PasswordField/PasswordField.module.css +0 -41
- package/dist/fields/RadioField/RadioField.module.css +0 -34
- package/dist/fields/SliderField/SliderField.module.css +0 -76
- package/dist/fields/SwitchField/SwitchField.module.css +0 -79
- package/dist/fields/fieldBase.module.css +0 -244
- package/dist/layout/Box/Box.module.css +0 -3
- package/dist/layout/Divider/Divider.module.css +0 -25
- package/dist/layout/Grid/Grid.module.css +0 -43
- package/dist/layout/Stack/Stack.module.css +0 -3
- package/dist/react/fieldGroup.module.css +0 -22
- package/dist/react/gridCell.module.css +0 -38
- package/dist/tokens/reset.css +0 -68
- package/dist/tokens/themes.css +0 -31
- 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
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
+
#### 01 · Basic Form
|
|
124
155
|
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
+
<AxenForm config={config} components={defaultComponentMap} onSubmit={console.log} />;
|
|
170
|
+
```
|
|
140
171
|
|
|
141
|
-
|
|
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
|
-
|
|
176
|
+
#### 02 · All 19 Field Types
|
|
144
177
|
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
160
|
-
type: '
|
|
161
|
-
label: '
|
|
162
|
-
|
|
163
|
-
|
|
214
|
+
name: 'price',
|
|
215
|
+
type: 'currency',
|
|
216
|
+
label: 'Price',
|
|
217
|
+
currency: 'USD',
|
|
218
|
+
locale: 'en-US',
|
|
219
|
+
colSpan: 4,
|
|
164
220
|
},
|
|
165
|
-
{ name: '
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
###
|
|
270
|
+
### INTERMEDIATE
|
|
173
271
|
|
|
174
|
-
|
|
272
|
+
#### 03 · Layout System
|
|
175
273
|
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
302
|
+
See [Layout System](#layout-system) for `Grid`, `Box`, `Stack`, `Divider`, and `Spacer` components.
|
|
192
303
|
|
|
193
|
-
|
|
304
|
+
#### 04 · Yup / Zod Validation
|
|
194
305
|
|
|
195
|
-
|
|
306
|
+
Bring your own validation library via adapters.
|
|
196
307
|
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
```
|
|
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
|
-
|
|
351
|
+
Hide fields dynamically based on form values. Hidden fields are excluded from validation and rendering.
|
|
231
352
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
383
|
+
### ADVANCED
|
|
244
384
|
|
|
245
|
-
|
|
385
|
+
#### 06 · Array Fields
|
|
246
386
|
|
|
247
|
-
|
|
387
|
+
Repeating field groups with add/remove controls.
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
248
390
|
import type { ArrayFieldConfig } from '@axenstudio/axen-form';
|
|
249
391
|
|
|
250
|
-
const
|
|
251
|
-
name: '
|
|
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
|
-
{
|
|
261
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
<
|
|
287
|
-
|
|
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
|
-
|
|
461
|
+
#### 08 · Payload Mapping
|
|
305
462
|
|
|
306
|
-
Control which fields are
|
|
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
|
|
472
|
+
// values: { name, email } — nothing else
|
|
316
473
|
}}
|
|
317
474
|
/>
|
|
318
475
|
|
|
319
|
-
// Dynamic whitelist
|
|
476
|
+
// Dynamic whitelist — different fields per accountType
|
|
320
477
|
<AxenForm
|
|
321
478
|
config={config}
|
|
322
479
|
components={defaultComponentMap}
|
|
323
480
|
payloadFields={{
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
531
|
+
import { useAxenForm } from '@axenstudio/axen-form';
|
|
338
532
|
|
|
339
|
-
function
|
|
533
|
+
function FullyCustomForm() {
|
|
340
534
|
const { store, formApi } = useAxenForm({
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
559
|
+
#### 10 · Custom Component
|
|
364
560
|
|
|
365
|
-
Override any built-in field
|
|
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
|
-
|
|
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
|
-
|
|
375
|
-
|
|
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: {
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
809
|
+
### Responsive colSpan
|
|
403
810
|
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
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
|
-
|
|
870
|
+
**Props:** `display`, `flexDirection`, `justifyContent`, `alignItems`, `flexWrap`, `gap`, `p`/`px`/`py`, `m`/`mx`/`my`.
|
|
433
871
|
|
|
434
|
-
|
|
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
|
-
|
|
885
|
+
**Props:** `direction` (row/column), `spacing`, `alignItems`, `justifyContent`, `divider`.
|
|
886
|
+
|
|
887
|
+
#### Divider
|
|
446
888
|
|
|
447
|
-
Horizontal
|
|
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
|
-
|
|
901
|
+
#### Spacer
|
|
460
902
|
|
|
461
|
-
Invisible spacing element
|
|
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
|
|
467
|
-
<
|
|
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 |
|
|
492
|
-
| ------- | --------- |
|
|
493
|
-
| `x` | `number` |
|
|
494
|
-
| `y` | `number` |
|
|
495
|
-
| `basis` | `number` |
|
|
496
|
-
| `grow` | `boolean` |
|
|
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
|
-
##
|
|
924
|
+
## Core Concepts
|
|
501
925
|
|
|
502
|
-
|
|
926
|
+
### Config-Driven Forms
|
|
503
927
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
:
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
949
|
+
### Reactive Form State
|
|
520
950
|
|
|
521
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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` |
|
|
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
|
|
566
|
-
|
|
|
567
|
-
| `name`
|
|
568
|
-
| `type`
|
|
569
|
-
| `label`
|
|
570
|
-
| `placeholder`
|
|
571
|
-
| `required`
|
|
572
|
-
| `requiredMessage`
|
|
573
|
-
| `disabled`
|
|
574
|
-
| `readOnly`
|
|
575
|
-
| `hidden`
|
|
576
|
-
| `component`
|
|
577
|
-
| `options`
|
|
578
|
-
| `
|
|
579
|
-
| `
|
|
580
|
-
| `
|
|
581
|
-
| `
|
|
582
|
-
| `
|
|
583
|
-
| `
|
|
584
|
-
| `
|
|
585
|
-
| `
|
|
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
|
|
590
|
-
| ---------------- |
|
|
591
|
-
| `useAxenForm` | `(
|
|
592
|
-
| `useField` | `(name) => { value, error, touched, onChange, onBlur }`
|
|
593
|
-
| `useFieldArray` | `(name) => {
|
|
594
|
-
| `useFormContext` | `() =>
|
|
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
|
|
599
|
-
|
|
|
600
|
-
| `mapPayload(values,
|
|
601
|
-
| `pickFields(values,
|
|
602
|
-
| `getStrippedFields(values,
|
|
603
|
-
| `get(obj, path, default?)`
|
|
604
|
-
| `set(obj, path, value)`
|
|
605
|
-
| `formatNumber(value,
|
|
606
|
-
| `formatDateLocale(isoDate, locale)`
|
|
607
|
-
| `
|
|
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
|
|
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 #
|
|
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
|
|
1099
|
+
│ └── FormContext.ts # React context
|
|
631
1100
|
│
|
|
632
|
-
├── fields/ #
|
|
633
|
-
│ ├──
|
|
634
|
-
│ ├──
|
|
635
|
-
│ ├──
|
|
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
|
-
│ ├──
|
|
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
|
|
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 |
|