@page-speed/forms 0.4.3 → 0.4.5
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 +158 -616
- package/dist/core.cjs +119 -27
- package/dist/core.cjs.map +1 -1
- package/dist/core.js +106 -14
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +119 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +106 -14
- package/dist/index.js.map +1 -1
- package/dist/inputs.cjs +519 -523
- package/dist/inputs.cjs.map +1 -1
- package/dist/inputs.d.cts +21 -54
- package/dist/inputs.d.ts +21 -54
- package/dist/inputs.js +518 -522
- package/dist/inputs.js.map +1 -1
- package/dist/integration.cjs +11 -3
- package/dist/integration.cjs.map +1 -1
- package/dist/integration.js +11 -3
- package/dist/integration.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,679 +1,221 @@
|
|
|
1
1
|

|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# `@page-speed/forms`
|
|
4
4
|
|
|
5
|
-
Type-safe form state
|
|
6
|
-
|
|
7
|
-
## Overview
|
|
8
|
-
|
|
9
|
-
OpenSite Page Speed Forms is a high-performance library designed to streamline form state management, validation, and submission handling in React applications. This library is part of OpenSite AI's open-source ecosystem, built for performance and open collaboration. By emphasizing type safety and modularity, it aligns with OpenSite's goal to create scalable, open, and developer-friendly performance tooling.
|
|
5
|
+
Type-safe, high-performance React form state and input components for OpenSite/DashTrack workloads.
|
|
10
6
|
|
|
11
7
|
[](https://www.npmjs.com/package/@page-speed/forms)
|
|
12
8
|
[](https://www.npmjs.com/package/@page-speed/forms)
|
|
13
9
|
[](./LICENSE)
|
|
14
|
-
[](./tsconfig.json)
|
|
15
|
-
[](#tree-shaking)
|
|
16
10
|
|
|
17
|
-
|
|
11
|
+
## Highlights
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
13
|
+
- Field-level reactivity via `@legendapp/state/react`
|
|
14
|
+
- Typed `useForm` and `useField` APIs
|
|
15
|
+
- Built-in input library (text, select, date, time, upload, rich text)
|
|
16
|
+
- Tree-shakable subpath exports (`/core`, `/inputs`, `/validation`, `/upload`, `/integration`)
|
|
17
|
+
- Validation rules and utilities (sync + async)
|
|
18
|
+
- Valibot adapter in a separate entrypoint (`/validation/valibot`)
|
|
19
|
+
- Tailwind token-based default UI aligned with ShadCN interaction patterns
|
|
26
20
|
|
|
27
21
|
## Installation
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
```bash
|
|
24
|
+
pnpm add @page-speed/forms
|
|
25
|
+
# or
|
|
32
26
|
npm install @page-speed/forms
|
|
33
27
|
```
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
-
|
|
29
|
+
Peer dependencies:
|
|
30
|
+
- `react >= 16.8.0`
|
|
31
|
+
- `react-dom >= 16.8.0`
|
|
37
32
|
|
|
38
33
|
## Quick Start
|
|
39
34
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
import
|
|
44
|
-
import {
|
|
35
|
+
```tsx
|
|
36
|
+
import * as React from "react";
|
|
37
|
+
import { Form, Field, useForm } from "@page-speed/forms";
|
|
38
|
+
import { TextInput, Select } from "@page-speed/forms/inputs";
|
|
39
|
+
import { required, email } from "@page-speed/forms/validation/rules";
|
|
45
40
|
|
|
46
|
-
function
|
|
41
|
+
export function ContactForm() {
|
|
47
42
|
const form = useForm({
|
|
48
|
-
initialValues: {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
initialValues: {
|
|
44
|
+
fullName: "",
|
|
45
|
+
email: "",
|
|
46
|
+
inquiryType: "",
|
|
47
|
+
},
|
|
48
|
+
validationSchema: {
|
|
49
|
+
fullName: required(),
|
|
50
|
+
email: [required(), email()],
|
|
51
|
+
inquiryType: required(),
|
|
52
|
+
},
|
|
53
|
+
onSubmit: async (values) => {
|
|
54
|
+
console.log(values);
|
|
55
|
+
},
|
|
52
56
|
});
|
|
53
57
|
|
|
54
58
|
return (
|
|
55
59
|
<Form form={form}>
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
<Field name="fullName" label="Full Name" required>
|
|
61
|
+
{({ field, meta }) => (
|
|
62
|
+
<TextInput
|
|
63
|
+
{...field}
|
|
64
|
+
placeholder="Your name"
|
|
65
|
+
error={Boolean(meta.touched && meta.error)}
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
</Field>
|
|
69
|
+
|
|
70
|
+
<Field name="email" label="Email" required>
|
|
71
|
+
{({ field, meta }) => (
|
|
72
|
+
<TextInput
|
|
73
|
+
{...field}
|
|
74
|
+
type="email"
|
|
75
|
+
placeholder="you@example.com"
|
|
76
|
+
error={Boolean(meta.touched && meta.error)}
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
79
|
+
</Field>
|
|
80
|
+
|
|
81
|
+
<Field name="inquiryType" label="Inquiry Type" required>
|
|
82
|
+
{({ field, meta }) => (
|
|
83
|
+
<Select
|
|
84
|
+
{...field}
|
|
85
|
+
options={[
|
|
86
|
+
{ label: "General", value: "general" },
|
|
87
|
+
{ label: "Sales", value: "sales" },
|
|
88
|
+
{ label: "Support", value: "support" },
|
|
89
|
+
]}
|
|
90
|
+
error={Boolean(meta.touched && meta.error)}
|
|
91
|
+
/>
|
|
92
|
+
)}
|
|
93
|
+
</Field>
|
|
94
|
+
|
|
95
|
+
<button type="submit" disabled={form.isSubmitting}>
|
|
96
|
+
Submit
|
|
97
|
+
</button>
|
|
63
98
|
</Form>
|
|
64
99
|
);
|
|
65
100
|
}
|
|
66
101
|
```
|
|
67
102
|
|
|
68
|
-
##
|
|
69
|
-
|
|
70
|
-
OpenSite Page Speed Forms can be customized with various options:
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
const form = useForm({
|
|
74
|
-
initialValues: { email: '' },
|
|
75
|
-
validationSchema: {
|
|
76
|
-
email: (value) => value.includes('@') ? undefined : 'Invalid email'
|
|
77
|
-
},
|
|
78
|
-
validateOn: 'onBlur',
|
|
79
|
-
revalidateOn: 'onChange',
|
|
80
|
-
onSubmit: (values) => console.log(values),
|
|
81
|
-
onError: (errors) => console.error(errors),
|
|
82
|
-
debug: true
|
|
83
|
-
});
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## Advanced Validation Features
|
|
87
|
-
|
|
88
|
-
### Cross-Field Validation
|
|
89
|
-
|
|
90
|
-
Validate fields that depend on other field values using the `crossFieldValidator` utility or by accessing `allValues` in your validator:
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
import { useForm, crossFieldValidator } from '@page-speed/forms/validation';
|
|
94
|
-
|
|
95
|
-
// Method 1: Using crossFieldValidator helper
|
|
96
|
-
const form = useForm({
|
|
97
|
-
initialValues: { password: '', confirmPassword: '' },
|
|
98
|
-
validationSchema: {
|
|
99
|
-
confirmPassword: crossFieldValidator(
|
|
100
|
-
['password', 'confirmPassword'],
|
|
101
|
-
(values) => {
|
|
102
|
-
if (values.password !== values.confirmPassword) {
|
|
103
|
-
return 'Passwords must match';
|
|
104
|
-
}
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
107
|
-
)
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Method 2: Direct access to allValues
|
|
112
|
-
const form = useForm({
|
|
113
|
-
initialValues: { password: '', confirmPassword: '' },
|
|
114
|
-
validationSchema: {
|
|
115
|
-
confirmPassword: (value, allValues) => {
|
|
116
|
-
if (value !== allValues.password) {
|
|
117
|
-
return 'Passwords must match';
|
|
118
|
-
}
|
|
119
|
-
return undefined;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
```
|
|
103
|
+
## Package Entry Points
|
|
124
104
|
|
|
125
|
-
###
|
|
126
|
-
|
|
127
|
-
Optimize async validators (like API calls) with built-in debouncing to prevent excessive requests:
|
|
128
|
-
|
|
129
|
-
```typescript
|
|
130
|
-
import { useForm, asyncValidator } from '@page-speed/forms/validation';
|
|
131
|
-
|
|
132
|
-
const checkUsernameAvailability = async (username: string) => {
|
|
133
|
-
const response = await fetch(`/api/check-username?username=${username}`);
|
|
134
|
-
const { available } = await response.json();
|
|
135
|
-
return available ? undefined : 'Username already taken';
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const form = useForm({
|
|
139
|
-
initialValues: { username: '' },
|
|
140
|
-
validationSchema: {
|
|
141
|
-
// Debounce async validation by 500ms
|
|
142
|
-
username: asyncValidator(
|
|
143
|
-
checkUsernameAvailability,
|
|
144
|
-
{ delay: 500, trailing: true }
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
```
|
|
105
|
+
### Main
|
|
106
|
+
- `@page-speed/forms`
|
|
149
107
|
|
|
150
|
-
|
|
151
|
-
- `
|
|
152
|
-
-
|
|
153
|
-
- `trailing`: Validate after delay expires (default: true)
|
|
154
|
-
|
|
155
|
-
The `asyncValidator` wrapper also includes automatic race condition prevention, ensuring only the latest validation result is used.
|
|
156
|
-
|
|
157
|
-
### Validation Rules Library
|
|
158
|
-
|
|
159
|
-
Use pre-built, tree-shakable validation rules for common scenarios:
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
import {
|
|
163
|
-
required,
|
|
164
|
-
email,
|
|
165
|
-
url,
|
|
166
|
-
phone,
|
|
167
|
-
minLength,
|
|
168
|
-
maxLength,
|
|
169
|
-
min,
|
|
170
|
-
max,
|
|
171
|
-
pattern,
|
|
172
|
-
matches,
|
|
173
|
-
oneOf,
|
|
174
|
-
creditCard,
|
|
175
|
-
postalCode,
|
|
176
|
-
alpha,
|
|
177
|
-
alphanumeric,
|
|
178
|
-
numeric,
|
|
179
|
-
integer,
|
|
180
|
-
compose
|
|
181
|
-
} from '@page-speed/forms/validation/rules';
|
|
182
|
-
|
|
183
|
-
const form = useForm({
|
|
184
|
-
initialValues: {
|
|
185
|
-
email: '',
|
|
186
|
-
password: '',
|
|
187
|
-
confirmPassword: '',
|
|
188
|
-
age: 0,
|
|
189
|
-
username: '',
|
|
190
|
-
cardNumber: ''
|
|
191
|
-
},
|
|
192
|
-
validationSchema: {
|
|
193
|
-
email: compose(
|
|
194
|
-
required({ message: 'Email is required' }),
|
|
195
|
-
email({ message: 'Invalid email format' })
|
|
196
|
-
),
|
|
197
|
-
password: compose(
|
|
198
|
-
required(),
|
|
199
|
-
minLength(8, { message: 'Password must be at least 8 characters' })
|
|
200
|
-
),
|
|
201
|
-
confirmPassword: matches('password', { message: 'Passwords must match' }),
|
|
202
|
-
age: compose(
|
|
203
|
-
required(),
|
|
204
|
-
numeric({ message: 'Age must be a number' }),
|
|
205
|
-
min(18, { message: 'Must be 18 or older' })
|
|
206
|
-
),
|
|
207
|
-
username: compose(
|
|
208
|
-
required(),
|
|
209
|
-
alphanumeric({ message: 'Only letters and numbers allowed' }),
|
|
210
|
-
minLength(3),
|
|
211
|
-
maxLength(20)
|
|
212
|
-
),
|
|
213
|
-
cardNumber: creditCard({ message: 'Invalid credit card number' })
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
**Available Validators:**
|
|
219
|
-
|
|
220
|
-
| Validator | Description | Example |
|
|
221
|
-
|-----------|-------------|---------|
|
|
222
|
-
| `required()` | Field must have a value | `required({ message: 'Required' })` |
|
|
223
|
-
| `email()` | Valid email format (RFC 5322) | `email()` |
|
|
224
|
-
| `url()` | Valid URL format | `url()` |
|
|
225
|
-
| `phone()` | US phone number format | `phone()` |
|
|
226
|
-
| `minLength(n)` | Minimum string/array length | `minLength(3)` |
|
|
227
|
-
| `maxLength(n)` | Maximum string/array length | `maxLength(100)` |
|
|
228
|
-
| `min(n)` | Minimum numeric value | `min(0)` |
|
|
229
|
-
| `max(n)` | Maximum numeric value | `max(100)` |
|
|
230
|
-
| `pattern(regex)` | Custom regex pattern | `pattern(/^[A-Z]+$/)` |
|
|
231
|
-
| `matches(field)` | Match another field | `matches('password')` |
|
|
232
|
-
| `oneOf(values)` | Value in allowed list | `oneOf(['a', 'b', 'c'])` |
|
|
233
|
-
| `creditCard()` | Valid credit card (Luhn) | `creditCard()` |
|
|
234
|
-
| `postalCode()` | US ZIP code format | `postalCode()` |
|
|
235
|
-
| `alpha()` | Alphabetic characters only | `alpha()` |
|
|
236
|
-
| `alphanumeric()` | Letters and numbers only | `alphanumeric()` |
|
|
237
|
-
| `numeric()` | Valid number | `numeric()` |
|
|
238
|
-
| `integer()` | Whole number | `integer()` |
|
|
239
|
-
| `compose(...)` | Combine multiple validators | `compose(required(), email())` |
|
|
240
|
-
|
|
241
|
-
### Custom Error Messages & Internationalization
|
|
242
|
-
|
|
243
|
-
Customize error messages globally for internationalization support:
|
|
244
|
-
|
|
245
|
-
```typescript
|
|
246
|
-
import { setErrorMessages } from '@page-speed/forms/validation/utils';
|
|
247
|
-
|
|
248
|
-
// Set custom messages (e.g., Spanish translations)
|
|
249
|
-
setErrorMessages({
|
|
250
|
-
required: 'Este campo es obligatorio',
|
|
251
|
-
email: 'Por favor ingrese un correo electrónico válido',
|
|
252
|
-
minLength: ({ min }) => `Debe tener al menos ${min} caracteres`,
|
|
253
|
-
maxLength: ({ max }) => `No debe exceder ${max} caracteres`,
|
|
254
|
-
phone: 'Por favor ingrese un número de teléfono válido'
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// Use with validation rules
|
|
258
|
-
import { required, email, minLength } from '@page-speed/forms/validation/rules';
|
|
259
|
-
|
|
260
|
-
const form = useForm({
|
|
261
|
-
initialValues: { email: '', password: '' },
|
|
262
|
-
validationSchema: {
|
|
263
|
-
email: compose(required(), email()),
|
|
264
|
-
password: compose(required(), minLength(8))
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
```
|
|
108
|
+
Exports:
|
|
109
|
+
- `useForm`, `useField`, `Form`, `Field`, `FormContext`
|
|
110
|
+
- core form/types interfaces
|
|
268
111
|
|
|
269
|
-
|
|
112
|
+
### Inputs
|
|
113
|
+
- `@page-speed/forms/inputs`
|
|
270
114
|
|
|
271
|
-
|
|
115
|
+
Exports:
|
|
116
|
+
- `TextInput`
|
|
117
|
+
- `TextArea`
|
|
118
|
+
- `Checkbox`
|
|
119
|
+
- `CheckboxGroup`
|
|
120
|
+
- `Radio`
|
|
121
|
+
- `Select`
|
|
122
|
+
- `MultiSelect`
|
|
123
|
+
- `DatePicker`
|
|
124
|
+
- `DateRangePicker`
|
|
125
|
+
- `TimePicker`
|
|
126
|
+
- `RichTextEditor`
|
|
127
|
+
- `FileInput`
|
|
272
128
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
});
|
|
279
|
-
```
|
|
129
|
+
### Validation
|
|
130
|
+
- `@page-speed/forms/validation`
|
|
131
|
+
- `@page-speed/forms/validation/rules`
|
|
132
|
+
- `@page-speed/forms/validation/utils`
|
|
133
|
+
- `@page-speed/forms/validation/valibot`
|
|
280
134
|
|
|
281
|
-
|
|
135
|
+
### Upload and Integration
|
|
136
|
+
- `@page-speed/forms/upload`
|
|
137
|
+
- `@page-speed/forms/integration`
|
|
282
138
|
|
|
283
|
-
|
|
139
|
+
## Input Notes
|
|
284
140
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
initialValues: { email: '' },
|
|
288
|
-
validationSchema: {
|
|
289
|
-
email: required({ message: 'Please provide your email address' })
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
```
|
|
141
|
+
### `TimePicker`
|
|
142
|
+
`TimePicker` now uses a native `input[type="time"]` UX internally.
|
|
293
143
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
import { when, required, minLength } from '@page-speed/forms/validation';
|
|
300
|
-
|
|
301
|
-
const form = useForm({
|
|
302
|
-
initialValues: { accountType: 'personal', companyName: '' },
|
|
303
|
-
validationSchema: {
|
|
304
|
-
// Only require company name for business accounts
|
|
305
|
-
companyName: when(
|
|
306
|
-
(allValues) => allValues.accountType === 'business',
|
|
307
|
-
compose(
|
|
308
|
-
required({ message: 'Company name is required for business accounts' }),
|
|
309
|
-
minLength(3)
|
|
310
|
-
)
|
|
311
|
-
)
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
```
|
|
144
|
+
- Accepts controlled values in `HH:mm` (24-hour) or `h:mm AM/PM` (12-hour)
|
|
145
|
+
- Emits `HH:mm` when `use24Hour` is `true`
|
|
146
|
+
- Emits `h:mm AM/PM` when `use24Hour` is `false`
|
|
315
147
|
|
|
316
|
-
|
|
148
|
+
### `DatePicker` and `DateRangePicker`
|
|
149
|
+
- Calendar popovers close on outside click
|
|
150
|
+
- Compact month/day layout using tokenized Tailwind classes
|
|
151
|
+
- `DateRangePicker` renders two months and highlights endpoints + in-range dates
|
|
317
152
|
|
|
318
|
-
|
|
153
|
+
### `Select` and `MultiSelect`
|
|
154
|
+
- Close on outside click
|
|
155
|
+
- Search support
|
|
156
|
+
- Option groups
|
|
157
|
+
- Selected options inside the menu use muted highlight styles
|
|
319
158
|
|
|
320
|
-
|
|
159
|
+
## Styling (Tailwind 4 + Semantic Tokens)
|
|
321
160
|
|
|
322
|
-
|
|
323
|
-
Standard text input with support for various types (text, email, password, etc.):
|
|
161
|
+
This library ships with Tailwind utility classes and semantic token class names.
|
|
324
162
|
|
|
325
|
-
|
|
326
|
-
import { TextInput } from '@page-speed/forms/inputs';
|
|
163
|
+
It is **not** a BEM-only unstyled package anymore.
|
|
327
164
|
|
|
328
|
-
|
|
329
|
-
{({ field }) => <TextInput {...field} type="email" placeholder="Enter email" />}
|
|
330
|
-
</Field>
|
|
331
|
-
```
|
|
165
|
+
### Base conventions
|
|
332
166
|
|
|
333
|
-
|
|
334
|
-
|
|
167
|
+
- Inputs/triggers are transparent shells with semantic borders/rings
|
|
168
|
+
- Fields with values (text-like controls) use `ring-2 ring-ring`
|
|
169
|
+
- Error states use destructive border/ring
|
|
170
|
+
- Dropdown selected rows use muted backgrounds
|
|
335
171
|
|
|
336
|
-
|
|
337
|
-
import { TextArea } from '@page-speed/forms/inputs';
|
|
172
|
+
### Autofill normalization
|
|
338
173
|
|
|
339
|
-
|
|
340
|
-
{({ field }) => <TextArea {...field} rows={5} placeholder="Enter description" />}
|
|
341
|
-
</Field>
|
|
342
|
-
```
|
|
174
|
+
Text-like controls apply autofill reset classes to avoid browser-injected background/text colors breaking your theme contrast.
|
|
343
175
|
|
|
344
|
-
|
|
345
|
-
Single checkbox or group of checkboxes:
|
|
346
|
-
|
|
347
|
-
```typescript
|
|
348
|
-
import { Checkbox, CheckboxGroup } from '@page-speed/forms/inputs';
|
|
349
|
-
|
|
350
|
-
// Single checkbox
|
|
351
|
-
<Field name="terms" label="Terms">
|
|
352
|
-
{({ field }) => <Checkbox {...field} label="I agree to the terms" />}
|
|
353
|
-
</Field>
|
|
354
|
-
|
|
355
|
-
// Checkbox group
|
|
356
|
-
<Field name="interests" label="Interests">
|
|
357
|
-
{({ field }) => (
|
|
358
|
-
<CheckboxGroup
|
|
359
|
-
{...field}
|
|
360
|
-
options={[
|
|
361
|
-
{ label: 'Sports', value: 'sports' },
|
|
362
|
-
{ label: 'Music', value: 'music' },
|
|
363
|
-
{ label: 'Travel', value: 'travel' }
|
|
364
|
-
]}
|
|
365
|
-
/>
|
|
366
|
-
)}
|
|
367
|
-
</Field>
|
|
368
|
-
```
|
|
176
|
+
See `INPUT_AUTOFILL_RESET_CLASSES` in `src/utils.ts`.
|
|
369
177
|
|
|
370
|
-
|
|
371
|
-
Radio button group:
|
|
372
|
-
|
|
373
|
-
```typescript
|
|
374
|
-
import { Radio } from '@page-speed/forms/inputs';
|
|
375
|
-
|
|
376
|
-
<Field name="plan" label="Select Plan">
|
|
377
|
-
{({ field }) => (
|
|
378
|
-
<Radio
|
|
379
|
-
{...field}
|
|
380
|
-
options={[
|
|
381
|
-
{ label: 'Basic', value: 'basic' },
|
|
382
|
-
{ label: 'Pro', value: 'pro' },
|
|
383
|
-
{ label: 'Enterprise', value: 'enterprise' }
|
|
384
|
-
]}
|
|
385
|
-
/>
|
|
386
|
-
)}
|
|
387
|
-
</Field>
|
|
388
|
-
```
|
|
178
|
+
### Token requirements
|
|
389
179
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
<Field name="country" label="Country">
|
|
398
|
-
{({ field }) => (
|
|
399
|
-
<Select
|
|
400
|
-
{...field}
|
|
401
|
-
options={[
|
|
402
|
-
{ label: 'United States', value: 'us' },
|
|
403
|
-
{ label: 'Canada', value: 'ca' },
|
|
404
|
-
{ label: 'United Kingdom', value: 'uk' }
|
|
405
|
-
]}
|
|
406
|
-
searchable
|
|
407
|
-
clearable
|
|
408
|
-
/>
|
|
409
|
-
)}
|
|
410
|
-
</Field>
|
|
411
|
-
|
|
412
|
-
// Multi-select
|
|
413
|
-
<Field name="skills" label="Skills">
|
|
414
|
-
{({ field }) => (
|
|
415
|
-
<Select
|
|
416
|
-
{...field}
|
|
417
|
-
multiple
|
|
418
|
-
options={[
|
|
419
|
-
{ label: 'JavaScript', value: 'js' },
|
|
420
|
-
{ label: 'TypeScript', value: 'ts' },
|
|
421
|
-
{ label: 'React', value: 'react' }
|
|
422
|
-
]}
|
|
423
|
-
searchable
|
|
424
|
-
/>
|
|
425
|
-
)}
|
|
426
|
-
</Field>
|
|
427
|
-
```
|
|
180
|
+
Ensure your app defines semantic tokens used in classes such as:
|
|
181
|
+
- `background`, `foreground`, `border`, `input`, `ring`
|
|
182
|
+
- `primary`, `primary-foreground`
|
|
183
|
+
- `muted`, `muted-foreground`
|
|
184
|
+
- `destructive`, `destructive-foreground`
|
|
185
|
+
- `popover`, `popover-foreground`
|
|
186
|
+
- `card`, `card-foreground`
|
|
428
187
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
#### DatePicker
|
|
432
|
-
Date selection with calendar popup:
|
|
433
|
-
|
|
434
|
-
```typescript
|
|
435
|
-
import { DatePicker } from '@page-speed/forms/inputs';
|
|
436
|
-
|
|
437
|
-
<Field name="birthdate" label="Birth Date">
|
|
438
|
-
{({ field }) => (
|
|
439
|
-
<DatePicker
|
|
440
|
-
{...field}
|
|
441
|
-
placeholder="Select date"
|
|
442
|
-
dateFormat="MM/dd/yyyy"
|
|
443
|
-
minDate={new Date(1900, 0, 1)}
|
|
444
|
-
maxDate={new Date()}
|
|
445
|
-
clearable
|
|
446
|
-
/>
|
|
447
|
-
)}
|
|
448
|
-
</Field>
|
|
449
|
-
```
|
|
188
|
+
For complete styling guidance, see [`docs/STYLES.md`](./docs/STYLES.md).
|
|
450
189
|
|
|
451
|
-
|
|
452
|
-
- `dateFormat`: Date display format (default: "MM/dd/yyyy")
|
|
453
|
-
- `minDate`, `maxDate`: Restrict selectable dates
|
|
454
|
-
- `isDateDisabled`: Custom function to disable specific dates
|
|
455
|
-
- `clearable`: Show clear button
|
|
456
|
-
- `showTodayButton`: Show "Today" button
|
|
457
|
-
|
|
458
|
-
#### TimePicker
|
|
459
|
-
Time selection with hour/minute/period selectors:
|
|
460
|
-
|
|
461
|
-
```typescript
|
|
462
|
-
import { TimePicker } from '@page-speed/forms/inputs';
|
|
463
|
-
|
|
464
|
-
<Field name="appointmentTime" label="Appointment Time">
|
|
465
|
-
{({ field }) => (
|
|
466
|
-
<TimePicker
|
|
467
|
-
{...field}
|
|
468
|
-
placeholder="Select time"
|
|
469
|
-
use24Hour={false}
|
|
470
|
-
minuteStep={15}
|
|
471
|
-
clearable
|
|
472
|
-
/>
|
|
473
|
-
)}
|
|
474
|
-
</Field>
|
|
475
|
-
```
|
|
190
|
+
## Validation Utilities
|
|
476
191
|
|
|
477
|
-
|
|
478
|
-
- `
|
|
479
|
-
- `
|
|
480
|
-
- `
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
Date range selection with start and end dates:
|
|
484
|
-
|
|
485
|
-
```typescript
|
|
486
|
-
import { DateRangePicker } from '@page-speed/forms/inputs';
|
|
487
|
-
|
|
488
|
-
<Field name="dateRange" label="Date Range">
|
|
489
|
-
{({ field }) => (
|
|
490
|
-
<DateRangePicker
|
|
491
|
-
{...field}
|
|
492
|
-
placeholder="Select date range"
|
|
493
|
-
separator=" - "
|
|
494
|
-
minDate={new Date()}
|
|
495
|
-
clearable
|
|
496
|
-
/>
|
|
497
|
-
)}
|
|
498
|
-
</Field>
|
|
499
|
-
```
|
|
192
|
+
Use built-in rules:
|
|
193
|
+
- `required`, `email`, `url`, `phone`
|
|
194
|
+
- `minLength`, `maxLength`, `min`, `max`
|
|
195
|
+
- `pattern`, `matches`, `oneOf`
|
|
196
|
+
- `creditCard`, `postalCode`, `alpha`, `alphanumeric`, `numeric`, `integer`
|
|
197
|
+
- `compose`
|
|
500
198
|
|
|
501
|
-
|
|
502
|
-
- `
|
|
503
|
-
- `
|
|
504
|
-
- `isDateDisabled`: Custom function to disable specific dates
|
|
505
|
-
- `clearable`: Show clear button
|
|
506
|
-
|
|
507
|
-
#### RichTextEditor
|
|
508
|
-
WYSIWYG and Markdown editor with toolbar:
|
|
509
|
-
|
|
510
|
-
```typescript
|
|
511
|
-
import { RichTextEditor } from '@page-speed/forms/inputs';
|
|
512
|
-
|
|
513
|
-
<Field name="content" label="Content">
|
|
514
|
-
{({ field }) => (
|
|
515
|
-
<RichTextEditor
|
|
516
|
-
{...field}
|
|
517
|
-
placeholder="Enter content..."
|
|
518
|
-
minHeight="200px"
|
|
519
|
-
maxHeight="600px"
|
|
520
|
-
allowModeSwitch
|
|
521
|
-
defaultMode="wysiwyg"
|
|
522
|
-
/>
|
|
523
|
-
)}
|
|
524
|
-
</Field>
|
|
525
|
-
```
|
|
199
|
+
Use utilities from `/validation/utils`:
|
|
200
|
+
- `debounce`, `asyncValidator`, `crossFieldValidator`, `when`
|
|
201
|
+
- `setErrorMessages`, `getErrorMessage`, `resetErrorMessages`
|
|
526
202
|
|
|
527
|
-
|
|
528
|
-
- `defaultMode`: "wysiwyg" or "markdown" (default: "wysiwyg")
|
|
529
|
-
- `allowModeSwitch`: Enable mode toggle button
|
|
530
|
-
- `minHeight`, `maxHeight`: Editor height constraints
|
|
531
|
-
- `customButtons`: Add custom toolbar buttons
|
|
532
|
-
|
|
533
|
-
**Features:**
|
|
534
|
-
- WYSIWYG mode: Bold, Italic, Underline, Headings, Lists, Links
|
|
535
|
-
- Markdown mode: Direct markdown editing
|
|
536
|
-
- Automatic HTML ↔ Markdown conversion
|
|
537
|
-
|
|
538
|
-
#### FileInput
|
|
539
|
-
File upload with drag-and-drop, progress indicators, and image cropping:
|
|
540
|
-
|
|
541
|
-
```typescript
|
|
542
|
-
import { FileInput } from '@page-speed/forms/inputs';
|
|
543
|
-
|
|
544
|
-
<Field name="avatar" label="Profile Picture">
|
|
545
|
-
{({ field }) => (
|
|
546
|
-
<FileInput
|
|
547
|
-
{...field}
|
|
548
|
-
accept="image/*"
|
|
549
|
-
maxSize={5 * 1024 * 1024} // 5MB
|
|
550
|
-
maxFiles={1}
|
|
551
|
-
showProgress
|
|
552
|
-
uploadProgress={uploadProgress}
|
|
553
|
-
enableCropping
|
|
554
|
-
cropAspectRatio={1}
|
|
555
|
-
onCropComplete={(file) => console.log('Cropped:', file)}
|
|
556
|
-
/>
|
|
557
|
-
)}
|
|
558
|
-
</Field>
|
|
559
|
-
```
|
|
203
|
+
## File Uploads
|
|
560
204
|
|
|
561
|
-
|
|
562
|
-
- `accept`: File type filter (e.g., "image/*", ".pdf")
|
|
563
|
-
- `multiple`: Allow multiple files
|
|
564
|
-
- `maxFiles`: Maximum number of files
|
|
565
|
-
- `maxSize`: Maximum file size in bytes
|
|
566
|
-
- `showPreview`: Show file previews
|
|
567
|
-
- `showProgress`: Display upload progress bars
|
|
568
|
-
- `uploadProgress`: Object mapping filenames to progress percentages
|
|
569
|
-
- `enableCropping`: Enable image cropping for image files
|
|
570
|
-
- `cropAspectRatio`: Crop aspect ratio (e.g., 16/9, 1 for square)
|
|
571
|
-
- `onCropComplete`: Callback when cropping is complete
|
|
572
|
-
|
|
573
|
-
**Features:**
|
|
574
|
-
- Drag-and-drop support
|
|
575
|
-
- File type and size validation
|
|
576
|
-
- Image previews with thumbnails
|
|
577
|
-
- Upload progress indicators with percentage
|
|
578
|
-
- Interactive image cropping with zoom
|
|
579
|
-
- Multiple file support
|
|
580
|
-
- Accessible file selection
|
|
581
|
-
|
|
582
|
-
### File Upload Implementation
|
|
583
|
-
|
|
584
|
-
The `FileInput` component uses a **two-phase upload process** optimized for Rails API integration. Files are uploaded immediately to temporary storage and return unique tokens, which are then associated with your form submission.
|
|
585
|
-
|
|
586
|
-
**Quick Example:**
|
|
205
|
+
`FileInput` supports validation, drag/drop, preview, and crop workflows.
|
|
587
206
|
|
|
588
|
-
|
|
589
|
-
|
|
207
|
+
For full two-phase upload patterns and serializer usage, see:
|
|
208
|
+
- [`docs/FILE_UPLOADS.md`](./docs/FILE_UPLOADS.md)
|
|
209
|
+
- `@page-speed/forms/integration`
|
|
590
210
|
|
|
591
|
-
|
|
592
|
-
const formData = new FormData();
|
|
593
|
-
formData.append("contact_form_upload[file_upload]", files[0]);
|
|
211
|
+
## Development
|
|
594
212
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const data = await response.json();
|
|
601
|
-
setUploadTokens([data.token]);
|
|
602
|
-
};
|
|
603
|
-
|
|
604
|
-
// In your form submission:
|
|
605
|
-
onSubmit: async (values) => {
|
|
606
|
-
await submitForm({
|
|
607
|
-
...values,
|
|
608
|
-
contact_form_upload_tokens: uploadTokens,
|
|
609
|
-
});
|
|
610
|
-
}
|
|
213
|
+
```bash
|
|
214
|
+
pnpm test:ci
|
|
215
|
+
pnpm build
|
|
216
|
+
pnpm type-check
|
|
611
217
|
```
|
|
612
218
|
|
|
613
|
-
**Comprehensive Guide:**
|
|
614
|
-
|
|
615
|
-
For complete file upload documentation, including:
|
|
616
|
-
- Two-phase upload process and flow diagrams
|
|
617
|
-
- Rails API integration with endpoint specifications
|
|
618
|
-
- Multiple working examples (resume uploads, image galleries, document forms)
|
|
619
|
-
- Progress tracking and error handling patterns
|
|
620
|
-
- Image cropping implementation
|
|
621
|
-
- File validation strategies
|
|
622
|
-
- Best practices and common patterns
|
|
623
|
-
- Troubleshooting guide
|
|
624
|
-
|
|
625
|
-
See the **[File Upload Guide](./docs/FILE_UPLOADS.md)** for detailed information.
|
|
626
|
-
|
|
627
|
-
## Styling
|
|
628
|
-
|
|
629
|
-
All components in `@page-speed/forms` are **intentionally unstyled** to provide maximum flexibility and framework-agnostic design. Components use predictable BEM class names (e.g., `.text-input`, `.select-trigger`, `.field-label`) as styling hooks, allowing you to apply any design system or custom styles.
|
|
630
|
-
|
|
631
|
-
**Quick Example:**
|
|
632
|
-
|
|
633
|
-
```css
|
|
634
|
-
/* Your custom CSS */
|
|
635
|
-
.text-input {
|
|
636
|
-
height: 2.25rem;
|
|
637
|
-
border: 1px solid #d1d5db;
|
|
638
|
-
border-radius: 0.375rem;
|
|
639
|
-
padding: 0.5rem 0.75rem;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
.text-input:focus {
|
|
643
|
-
outline: none;
|
|
644
|
-
border-color: #3b82f6;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
.text-input--error {
|
|
648
|
-
border-color: #ef4444;
|
|
649
|
-
}
|
|
650
|
-
```
|
|
651
|
-
|
|
652
|
-
**Comprehensive Guide:**
|
|
653
|
-
|
|
654
|
-
For complete styling documentation, including:
|
|
655
|
-
- BEM class reference for all components
|
|
656
|
-
- Multiple styling approaches (Vanilla CSS, Tailwind, CSS Modules, CSS-in-JS)
|
|
657
|
-
- Complete examples (shadcn/ui, Material Design, custom brands)
|
|
658
|
-
- Best practices and common patterns
|
|
659
|
-
- Dark mode support
|
|
660
|
-
|
|
661
|
-
See the **[Styling Guide](./docs/STYLES.md)** for detailed information.
|
|
662
|
-
|
|
663
|
-
## Performance Notes
|
|
664
|
-
|
|
665
|
-
Performance is a core facet of everything we build at OpenSite AI. The library is optimized for minimal re-renders and efficient form state updates, ensuring your applications remain responsive and fast.
|
|
666
|
-
|
|
667
|
-
## Contributing
|
|
668
|
-
|
|
669
|
-
We welcome contributions from the community to enhance OpenSite Page Speed Forms. Please refer to our [GitHub repository](https://github.com/opensite-ai) for guidelines and more information on how to get involved.
|
|
670
|
-
|
|
671
219
|
## License
|
|
672
220
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
## Related Projects
|
|
676
|
-
|
|
677
|
-
- [Domain Extractor](https://github.com/opensite-ai/domain_extractor)
|
|
678
|
-
- [Page Speed Hooks](https://github.com/opensite-ai/page-speed-hooks)
|
|
679
|
-
- Visit [opensite.ai](https://opensite.ai) for more tools and information.
|
|
221
|
+
MIT. See [`LICENSE`](./LICENSE).
|