@page-speed/forms 0.4.4 → 0.4.6

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 CHANGED
@@ -1,679 +1,221 @@
1
1
  ![Page Speed React Forms](https://octane.cdn.ing/api/v1/images/transform?url=https://cdn.ing/assets/i/r/286339/nwqgw37pigfluhcmmjmpql3yj9y4/github.png&q=90)
2
2
 
3
- # ⚡️ @page-speed/forms
3
+ # `@page-speed/forms`
4
4
 
5
- Type-safe form state management and validation for React applications.
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
  [![npm version](https://img.shields.io/npm/v/@page-speed/forms?style=flat-square)](https://www.npmjs.com/package/@page-speed/forms)
12
8
  [![npm downloads](https://img.shields.io/npm/dm/@page-speed/forms?style=flat-square)](https://www.npmjs.com/package/@page-speed/forms)
13
9
  [![License](https://img.shields.io/npm/l/@page-speed/forms?style=flat-square)](./LICENSE)
14
- [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?style=flat-square)](./tsconfig.json)
15
- [![Tree-Shakeable](https://img.shields.io/badge/Tree%20Shakeable-Yes-brightgreen?style=flat-square)](#tree-shaking)
16
10
 
17
- Learn more at [OpenSite.ai Developers](https://opensite.ai/developers).
11
+ ## Highlights
18
12
 
19
- ## Key Features
20
-
21
- - Type-safe form state management with TypeScript.
22
- - Flexible validation schemas supporting both synchronous and asynchronous validation.
23
- - Modular useForm and useField hooks for complete form and field control.
24
- - Built-in support for form submission and error handling.
25
- - Configurable validation modes: `onChange`, `onBlur`, and `onSubmit`.
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
- To install OpenSite Page Speed Forms, ensure you have Node.js and npm installed, then run:
30
-
31
- ```
23
+ ```bash
24
+ pnpm add @page-speed/forms
25
+ # or
32
26
  npm install @page-speed/forms
33
27
  ```
34
28
 
35
- Dependencies:
36
- - React
29
+ Peer dependencies:
30
+ - `react >= 16.8.0`
31
+ - `react-dom >= 16.8.0`
37
32
 
38
33
  ## Quick Start
39
34
 
40
- Here is a basic example to get started with OpenSite Page Speed Forms in your React application:
41
-
42
- ```typescript
43
- import React from 'react';
44
- import { useForm, Form } from '@page-speed/forms';
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 MyForm() {
41
+ export function ContactForm() {
47
42
  const form = useForm({
48
- initialValues: { email: '' },
49
- onSubmit: (values) => {
50
- console.log('Form Submitted:', values);
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
- <input
57
- name="email"
58
- value={form.values.email}
59
- onChange={(e) => form.setFieldValue('email', e.target.value)}
60
- onBlur={() => form.setFieldTouched('email', true)}
61
- />
62
- <button type="submit">Submit</button>
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
- ## Configuration or Advanced Usage
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
- ### Async Validation with Debouncing
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
- **Debounce Options:**
151
- - `delay`: Milliseconds to wait (default: 300ms)
152
- - `leading`: Validate immediately on first change (default: false)
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
- **Message Template Functions:**
112
+ ### Inputs
113
+ - `@page-speed/forms/inputs`
270
114
 
271
- Error messages support template functions with parameter interpolation:
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
- ```typescript
274
- setErrorMessages({
275
- minLength: ({ min }) => `Must be at least ${min} characters`,
276
- max: ({ max }) => `Cannot exceed ${max}`,
277
- matches: ({ field }) => `Must match ${field}`
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
- **Per-Field Custom Messages:**
135
+ ### Upload and Integration
136
+ - `@page-speed/forms/upload`
137
+ - `@page-speed/forms/integration`
282
138
 
283
- Override global messages on a per-field basis:
139
+ ## Input Notes
284
140
 
285
- ```typescript
286
- const form = useForm({
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
- ### Conditional Validation
295
-
296
- Validate fields only when certain conditions are met:
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
- ## Built-in Input Components
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
- `@page-speed/forms` includes a comprehensive set of accessible, production-ready input components that work seamlessly with the form hooks.
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
- ### Basic Inputs
159
+ ## Styling (Tailwind 4 + Semantic Tokens)
321
160
 
322
- #### TextInput
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
- ```typescript
326
- import { TextInput } from '@page-speed/forms/inputs';
163
+ It is **not** a BEM-only unstyled package anymore.
327
164
 
328
- <Field name="email" label="Email">
329
- {({ field }) => <TextInput {...field} type="email" placeholder="Enter email" />}
330
- </Field>
331
- ```
165
+ ### Base conventions
332
166
 
333
- #### TextArea
334
- Multi-line text input:
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
- ```typescript
337
- import { TextArea } from '@page-speed/forms/inputs';
172
+ ### Autofill normalization
338
173
 
339
- <Field name="description" label="Description">
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
- #### Checkbox & CheckboxGroup
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
- #### Radio
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
- #### Select
391
- Dropdown select with support for single and multi-select:
392
-
393
- ```typescript
394
- import { Select } from '@page-speed/forms/inputs';
395
-
396
- // Single select
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
- ### Advanced Inputs
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
- **Props:**
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
- **Props:**
478
- - `use24Hour`: Use 24-hour format (default: false)
479
- - `minuteStep`: Minute increment (default: 1)
480
- - `clearable`: Show clear button
481
-
482
- #### DateRangePicker
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
- **Props:**
502
- - `separator`: String between start and end dates (default: " - ")
503
- - `minDate`, `maxDate`: Restrict selectable dates
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
- **Props:**
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
- **Props:**
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
- ```tsx
589
- const [uploadTokens, setUploadTokens] = useState<string[]>([]);
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
- const handleFileUpload = async (files: File[]) => {
592
- const formData = new FormData();
593
- formData.append("contact_form_upload[file_upload]", files[0]);
211
+ ## Development
594
212
 
595
- const response = await fetch("/api/contact_form_uploads", {
596
- method: "POST",
597
- body: formData,
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
- Licensed under the BSD 3-Clause License. See the [LICENSE](./LICENSE) file for details.
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).