@page-speed/forms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +469 -0
  3. package/dist/builder.cjs +4 -0
  4. package/dist/builder.cjs.map +1 -0
  5. package/dist/builder.d.cts +2 -0
  6. package/dist/builder.d.ts +2 -0
  7. package/dist/builder.js +3 -0
  8. package/dist/builder.js.map +1 -0
  9. package/dist/chunk-2FXAQT7S.cjs +236 -0
  10. package/dist/chunk-2FXAQT7S.cjs.map +1 -0
  11. package/dist/chunk-A3UV7BIN.js +357 -0
  12. package/dist/chunk-A3UV7BIN.js.map +1 -0
  13. package/dist/chunk-P37YLBFA.cjs +138 -0
  14. package/dist/chunk-P37YLBFA.cjs.map +1 -0
  15. package/dist/chunk-WHQMBQNI.js +127 -0
  16. package/dist/chunk-WHQMBQNI.js.map +1 -0
  17. package/dist/chunk-YTTOWHBZ.js +217 -0
  18. package/dist/chunk-YTTOWHBZ.js.map +1 -0
  19. package/dist/chunk-ZQCPEOB6.cjs +382 -0
  20. package/dist/chunk-ZQCPEOB6.cjs.map +1 -0
  21. package/dist/core.cjs +28 -0
  22. package/dist/core.cjs.map +1 -0
  23. package/dist/core.d.cts +143 -0
  24. package/dist/core.d.ts +143 -0
  25. package/dist/core.js +3 -0
  26. package/dist/core.js.map +1 -0
  27. package/dist/index.cjs +28 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.d.cts +3 -0
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.js +3 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/inputs.cjs +555 -0
  34. package/dist/inputs.cjs.map +1 -0
  35. package/dist/inputs.d.cts +433 -0
  36. package/dist/inputs.d.ts +433 -0
  37. package/dist/inputs.js +529 -0
  38. package/dist/inputs.js.map +1 -0
  39. package/dist/integration.cjs +4 -0
  40. package/dist/integration.cjs.map +1 -0
  41. package/dist/integration.d.cts +2 -0
  42. package/dist/integration.d.ts +2 -0
  43. package/dist/integration.js +3 -0
  44. package/dist/integration.js.map +1 -0
  45. package/dist/types-Cw5CeZP-.d.cts +387 -0
  46. package/dist/types-Cw5CeZP-.d.ts +387 -0
  47. package/dist/upload.cjs +4 -0
  48. package/dist/upload.cjs.map +1 -0
  49. package/dist/upload.d.cts +2 -0
  50. package/dist/upload.d.ts +2 -0
  51. package/dist/upload.js +3 -0
  52. package/dist/upload.js.map +1 -0
  53. package/dist/validation-rules.cjs +80 -0
  54. package/dist/validation-rules.cjs.map +1 -0
  55. package/dist/validation-rules.d.cts +123 -0
  56. package/dist/validation-rules.d.ts +123 -0
  57. package/dist/validation-rules.js +3 -0
  58. package/dist/validation-rules.js.map +1 -0
  59. package/dist/validation-utils.cjs +48 -0
  60. package/dist/validation-utils.cjs.map +1 -0
  61. package/dist/validation-utils.d.cts +166 -0
  62. package/dist/validation-utils.d.ts +166 -0
  63. package/dist/validation-utils.js +3 -0
  64. package/dist/validation-utils.js.map +1 -0
  65. package/dist/validation-valibot.cjs +94 -0
  66. package/dist/validation-valibot.cjs.map +1 -0
  67. package/dist/validation-valibot.d.cts +92 -0
  68. package/dist/validation-valibot.d.ts +92 -0
  69. package/dist/validation-valibot.js +91 -0
  70. package/dist/validation-valibot.js.map +1 -0
  71. package/dist/validation.cjs +121 -0
  72. package/dist/validation.cjs.map +1 -0
  73. package/dist/validation.d.cts +4 -0
  74. package/dist/validation.d.ts +4 -0
  75. package/dist/validation.js +4 -0
  76. package/dist/validation.js.map +1 -0
  77. package/package.json +133 -0
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, OpenSite AI. All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,469 @@
1
+ # @page-speed/forms
2
+
3
+ Ultra-high-performance React form library with field-level reactivity and tree-shakable architecture.
4
+
5
+ ## Features
6
+
7
+ - =� **Field-Level Reactivity**: Only re-render the specific field that changed (~1 re-render per change vs ~10 for traditional hooks)
8
+ - =� **Tree-Shakable**: Import only what you need - Core module starts at 13 KB gzipped
9
+ - � **Built on @legendapp/state**: Observable-based state management for optimal performance
10
+ -  **Valibot Integration**: Lightweight validation (95% smaller than Zod)
11
+ - <� **TypeScript-First**: Full type safety with comprehensive type definitions
12
+ -  **Accessible**: ARIA attributes and semantic HTML out of the box
13
+ - = **Progressive Enhancement**: Forms work without JavaScript
14
+ - <� **Unstyled**: Bring your own styles - no CSS to override
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ # Using pnpm (recommended)
20
+ pnpm add @page-speed/forms
21
+
22
+ # Optional: Add validation library
23
+ pnpm add valibot
24
+
25
+ # Optional: Add state management (peer dependency)
26
+ pnpm add @legendapp/state
27
+ ```
28
+
29
+ ## Bundle Sizes
30
+
31
+ All sizes shown are **minified + gzipped** with dependencies:
32
+
33
+ - **Core** (useForm, Form, Field, useField): 13.11 KB
34
+ - **TextInput**: 502 B
35
+ - **Valibot Adapter**: 392 B
36
+ - **Full Bundle**: 13.25 KB
37
+
38
+ ## Quick Start
39
+
40
+ ### Basic Form
41
+
42
+ ```tsx
43
+ import { useForm, Form, Field } from '@page-speed/forms';
44
+ import { TextInput } from '@page-speed/forms/inputs';
45
+
46
+ function LoginForm() {
47
+ const form = useForm({
48
+ initialValues: {
49
+ email: '',
50
+ password: '',
51
+ },
52
+ validationSchema: {
53
+ email: (value) => !value ? 'Required' : undefined,
54
+ password: (value) => value.length < 8 ? 'Too short' : undefined,
55
+ },
56
+ onSubmit: async (values) => {
57
+ await login(values);
58
+ },
59
+ });
60
+
61
+ return (
62
+ <Form form={form}>
63
+ <Field name="email" label="Email">
64
+ {({ field, meta }) => (
65
+ <>
66
+ <TextInput {...field} type="email" error={!!meta.error} />
67
+ {meta.error && <span>{meta.error}</span>}
68
+ </>
69
+ )}
70
+ </Field>
71
+
72
+ <Field name="password" label="Password">
73
+ {({ field, meta }) => (
74
+ <>
75
+ <TextInput {...field} type="password" error={!!meta.error} />
76
+ {meta.error && <span>{meta.error}</span>}
77
+ </>
78
+ )}
79
+ </Field>
80
+
81
+ <button type="submit" disabled={form.isSubmitting}>
82
+ Submit
83
+ </button>
84
+ </Form>
85
+ );
86
+ }
87
+ ```
88
+
89
+ ### With Valibot Validation
90
+
91
+ ```tsx
92
+ import { useForm } from '@page-speed/forms';
93
+ import { createValibotSchema } from '@page-speed/forms/validation/valibot';
94
+ import * as v from 'valibot';
95
+
96
+ const LoginSchema = v.object({
97
+ email: v.pipe(v.string(), v.email('Invalid email')),
98
+ password: v.pipe(v.string(), v.minLength(8, 'Too short')),
99
+ });
100
+
101
+ function LoginForm() {
102
+ const form = useForm({
103
+ initialValues: { email: '', password: '' },
104
+ validationSchema: createValibotSchema(LoginSchema),
105
+ onSubmit: async (values) => {
106
+ await login(values);
107
+ },
108
+ });
109
+
110
+ // ... rest of form
111
+ }
112
+ ```
113
+
114
+ ## API Reference
115
+
116
+ ### `useForm(options)`
117
+
118
+ Main hook for creating form state.
119
+
120
+ **Options:**
121
+
122
+ - `initialValues` (required): Initial form values
123
+ - `validationSchema`: Schema mapping field names to validators
124
+ - `validateOn`: When to validate - `"onBlur"` (default) | `"onChange"` | `"onSubmit"`
125
+ - `revalidateOn`: When to revalidate after first validation - `"onChange"` (default) | `"onBlur"`
126
+ - `onSubmit` (required): Submit handler function
127
+ - `onError`: Error handler for validation failures
128
+ - `debug`: Enable debug logging
129
+
130
+ **Returns:**
131
+
132
+ ```typescript
133
+ {
134
+ // State
135
+ values: T;
136
+ errors: FormErrors<T>;
137
+ touched: TouchedFields<T>;
138
+ isSubmitting: boolean;
139
+ isValid: boolean;
140
+ isDirty: boolean;
141
+ status: 'idle' | 'submitting' | 'success' | 'error';
142
+
143
+ // Actions
144
+ handleSubmit: (e?: React.FormEvent) => Promise<void>;
145
+ setFieldValue: (field: keyof T, value: T[field]) => void;
146
+ setFieldError: (field: keyof T, error: string) => void;
147
+ setFieldTouched: (field: keyof T, touched: boolean) => void;
148
+ validateForm: () => Promise<FormErrors<T>>;
149
+ validateField: (field: keyof T) => Promise<string | undefined>;
150
+ resetForm: () => void;
151
+ getFieldProps: (field: keyof T) => FieldInputProps;
152
+ getFieldMeta: (field: keyof T) => FieldMeta;
153
+ }
154
+ ```
155
+
156
+ ### `<Form>`
157
+
158
+ Progressive enhancement wrapper component.
159
+
160
+ ```tsx
161
+ <Form
162
+ form={form}
163
+ action="/api/endpoint" // Fallback for no-JS
164
+ method="post" // Fallback for no-JS
165
+ className="my-form"
166
+ >
167
+ {/* fields */}
168
+ </Form>
169
+ ```
170
+
171
+ ### `<Field>`
172
+
173
+ Field wrapper with label, description, and error display.
174
+
175
+ ```tsx
176
+ <Field
177
+ name="email"
178
+ label="Email Address"
179
+ description="We'll never share your email"
180
+ validate={(value) => !value ? 'Required' : undefined}
181
+ >
182
+ {({ field, meta, helpers }) => (
183
+ <TextInput {...field} error={!!meta.error} />
184
+ )}
185
+ </Field>
186
+ ```
187
+
188
+ ### `useField(options)`
189
+
190
+ Field-level hook for accessing field state.
191
+
192
+ ```tsx
193
+ const { field, meta, helpers } = useField({
194
+ name: 'email',
195
+ validate: (value) => !value ? 'Required' : undefined,
196
+ transform: (value) => value.toLowerCase(),
197
+ });
198
+ ```
199
+
200
+ ### `<TextInput>`
201
+
202
+ Lightweight, accessible text input component.
203
+
204
+ ```tsx
205
+ <TextInput
206
+ name="email"
207
+ value={value}
208
+ onChange={onChange}
209
+ onBlur={onBlur}
210
+ type="email"
211
+ placeholder="you@example.com"
212
+ error={hasError}
213
+ disabled={false}
214
+ required={true}
215
+ />
216
+ ```
217
+
218
+ ## Validation
219
+
220
+ ### Inline Validators
221
+
222
+ ```tsx
223
+ const form = useForm({
224
+ initialValues: { email: '' },
225
+ validationSchema: {
226
+ email: (value) => {
227
+ if (!value) return 'Required';
228
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
229
+ return 'Invalid email';
230
+ }
231
+ return undefined;
232
+ },
233
+ },
234
+ onSubmit: async (values) => { /* ... */ },
235
+ });
236
+ ```
237
+
238
+ ### Async Validation
239
+
240
+ ```tsx
241
+ const form = useForm({
242
+ initialValues: { username: '' },
243
+ validationSchema: {
244
+ username: async (value) => {
245
+ const exists = await checkUsernameExists(value);
246
+ return exists ? 'Username taken' : undefined;
247
+ },
248
+ },
249
+ onSubmit: async (values) => { /* ... */ },
250
+ });
251
+ ```
252
+
253
+ ### Multiple Validators per Field
254
+
255
+ ```tsx
256
+ const form = useForm({
257
+ initialValues: { password: '' },
258
+ validationSchema: {
259
+ password: [
260
+ (value) => !value ? 'Required' : undefined,
261
+ (value) => value.length < 8 ? 'Too short' : undefined,
262
+ (value) => !/[A-Z]/.test(value) ? 'Needs uppercase' : undefined,
263
+ ],
264
+ },
265
+ onSubmit: async (values) => { /* ... */ },
266
+ });
267
+ ```
268
+
269
+ ### Valibot Integration
270
+
271
+ ```tsx
272
+ import { createValibotSchema } from '@page-speed/forms/validation/valibot';
273
+ import * as v from 'valibot';
274
+
275
+ const schema = v.object({
276
+ email: v.pipe(
277
+ v.string(),
278
+ v.email('Invalid email'),
279
+ v.endsWith('@company.com', 'Must be company email')
280
+ ),
281
+ age: v.pipe(
282
+ v.number(),
283
+ v.minValue(18, 'Must be 18+')
284
+ ),
285
+ });
286
+
287
+ const form = useForm({
288
+ initialValues: { email: '', age: 0 },
289
+ validationSchema: createValibotSchema(schema),
290
+ onSubmit: async (values) => { /* ... */ },
291
+ });
292
+ ```
293
+
294
+ ## Advanced Usage
295
+
296
+ ### Conditional Validation
297
+
298
+ ```tsx
299
+ const form = useForm({
300
+ initialValues: {
301
+ contactMethod: 'email',
302
+ email: '',
303
+ phone: '',
304
+ },
305
+ validationSchema: {
306
+ email: (value, allValues) => {
307
+ if (allValues.contactMethod === 'email' && !value) {
308
+ return 'Email required when email is selected';
309
+ }
310
+ return undefined;
311
+ },
312
+ phone: (value, allValues) => {
313
+ if (allValues.contactMethod === 'phone' && !value) {
314
+ return 'Phone required when phone is selected';
315
+ }
316
+ return undefined;
317
+ },
318
+ },
319
+ onSubmit: async (values) => { /* ... */ },
320
+ });
321
+ ```
322
+
323
+ ### Dynamic Forms
324
+
325
+ ```tsx
326
+ function DynamicForm() {
327
+ const form = useForm({
328
+ initialValues: {
329
+ contacts: [{ name: '', email: '' }],
330
+ },
331
+ onSubmit: async (values) => { /* ... */ },
332
+ });
333
+
334
+ return (
335
+ <Form form={form}>
336
+ {form.values.contacts.map((contact, index) => (
337
+ <div key={index}>
338
+ <Field name={`contacts.${index}.name`}>
339
+ {({ field }) => <TextInput {...field} />}
340
+ </Field>
341
+ <Field name={`contacts.${index}.email`}>
342
+ {({ field }) => <TextInput {...field} type="email" />}
343
+ </Field>
344
+ </div>
345
+ ))}
346
+ <button
347
+ type="button"
348
+ onClick={() => {
349
+ form.setFieldValue('contacts', [
350
+ ...form.values.contacts,
351
+ { name: '', email: '' },
352
+ ]);
353
+ }}
354
+ >
355
+ Add Contact
356
+ </button>
357
+ </Form>
358
+ );
359
+ }
360
+ ```
361
+
362
+ ### Form Helpers in onSubmit
363
+
364
+ ```tsx
365
+ const form = useForm({
366
+ initialValues: { email: '' },
367
+ onSubmit: async (values, helpers) => {
368
+ try {
369
+ await api.submit(values);
370
+ helpers.resetForm();
371
+ } catch (error) {
372
+ if (error.code === 'EMAIL_EXISTS') {
373
+ helpers.setFieldError('email', 'Email already exists');
374
+ } else {
375
+ helpers.setErrors({ email: 'Unexpected error' });
376
+ }
377
+ }
378
+ },
379
+ });
380
+ ```
381
+
382
+ ## Tree-Shaking
383
+
384
+ The library is designed for optimal tree-shaking. Import only what you need:
385
+
386
+ ```tsx
387
+ // Import core functionality (13.11 KB)
388
+ import { useForm, Form, Field } from '@page-speed/forms/core';
389
+
390
+ // Import input components separately (502 B)
391
+ import { TextInput } from '@page-speed/forms/inputs';
392
+
393
+ // Import validation adapters separately (392 B)
394
+ import { createValibotSchema } from '@page-speed/forms/validation/valibot';
395
+
396
+ // Or import from main entry point
397
+ import { useForm } from '@page-speed/forms';
398
+ ```
399
+
400
+ ## Performance
401
+
402
+ The library uses @legendapp/state for optimal performance:
403
+
404
+ - **~1 re-render per change** vs ~10 for traditional hooks
405
+ - **Observable-based state**: Fine-grained reactivity at the field level
406
+ - **No unnecessary re-renders**: Parent form doesn't re-render when child field changes
407
+ - **Efficient validation**: Debounced and memoized by default
408
+
409
+ ## Progressive Enhancement
410
+
411
+ Forms work without JavaScript by using native HTML form submission:
412
+
413
+ ```tsx
414
+ <Form
415
+ form={form}
416
+ action="/api/endpoint" // Used when JS disabled
417
+ method="post"
418
+ >
419
+ {/* fields */}
420
+ </Form>
421
+ ```
422
+
423
+ When JavaScript is available, `onSubmit` handles the submission. When JavaScript is disabled, the native HTML form submission takes over.
424
+
425
+ ## Browser Support
426
+
427
+ - Modern browsers (Chrome, Firefox, Safari, Edge)
428
+ - Requires ES2020 support
429
+ - No IE11 support
430
+
431
+ ## TypeScript
432
+
433
+ Fully typed with comprehensive type definitions:
434
+
435
+ ```tsx
436
+ import type {
437
+ FormValues,
438
+ FormErrors,
439
+ TouchedFields,
440
+ ValidationSchema,
441
+ FieldValidator,
442
+ UseFormOptions,
443
+ UseFormReturn,
444
+ } from '@page-speed/forms/core';
445
+ ```
446
+
447
+ ## Examples
448
+
449
+ See the [`examples/`](./examples) directory for more complete examples including:
450
+ - Basic forms with inline validation
451
+ - Valibot schema integration
452
+ - Dynamic forms with conditional fields
453
+ - Progressive enhancement
454
+ - Async validation
455
+
456
+ ## License
457
+
458
+ MIT
459
+
460
+ ## Contributing
461
+
462
+ Contributions welcome! Please read our contributing guidelines first.
463
+
464
+ ## Credits
465
+
466
+ Built with:
467
+ - [@legendapp/state](https://legendapp.com/open-source/state/) - Observable state management
468
+ - [Valibot](https://valibot.dev/) - Lightweight validation library
469
+ - [tsup](https://tsup.egoist.dev/) - TypeScript bundler
@@ -0,0 +1,4 @@
1
+ 'use strict';
2
+
3
+ //# sourceMappingURL=builder.cjs.map
4
+ //# sourceMappingURL=builder.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"builder.cjs"}
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,3 @@
1
+
2
+ //# sourceMappingURL=builder.js.map
3
+ //# sourceMappingURL=builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"builder.js"}