performa 1.0.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.
package/README.md ADDED
@@ -0,0 +1,850 @@
1
+ # performa
2
+
3
+ A lightweight, framework-agnostic React form library with built-in server-side validation, TypeScript support, and adapters for popular React frameworks.
4
+
5
+ ## Features
6
+
7
+ - **Framework Agnostic**: Works with Next.js, React Router, TanStack Start, and any React framework
8
+ - **Server-Side Validation**: Secure form validation with comprehensive validation rules
9
+ - **TypeScript First**: Full type safety with excellent IDE autocomplete
10
+ - **Accessible**: Built-in ARIA attributes and keyboard navigation
11
+ - **Themeable**: Fully customizable with Tailwind CSS or custom styling
12
+ - **Lightweight**: Core library is only 7.69 KB (brotli compressed)
13
+ - **File Uploads**: Built-in support for file validation and previews
14
+ - **Dark Mode**: Native dark mode support out of the box
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install performa
20
+ ```
21
+
22
+ ### Framework-Specific Installation
23
+
24
+ For Next.js (App Router):
25
+ ```bash
26
+ npm install performa react-dom
27
+ ```
28
+
29
+ For React Router:
30
+ ```bash
31
+ npm install performa react-router
32
+ ```
33
+
34
+ For TanStack Start:
35
+ ```bash
36
+ npm install performa @tanstack/start
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ### 1. Define Your Form Configuration
42
+
43
+ ```typescript
44
+ import { FormConfig } from 'performa';
45
+
46
+ const loginForm = {
47
+ key: 'login',
48
+ method: 'post',
49
+ action: '/api/login',
50
+ fields: {
51
+ email: {
52
+ type: 'email',
53
+ label: 'Email Address',
54
+ placeholder: 'Enter your email',
55
+ rules: {
56
+ required: true,
57
+ isEmail: true,
58
+ },
59
+ },
60
+ password: {
61
+ type: 'password',
62
+ label: 'Password',
63
+ placeholder: 'Enter your password',
64
+ rules: {
65
+ required: true,
66
+ minLength: 8,
67
+ },
68
+ },
69
+ remember: {
70
+ type: 'checkbox',
71
+ label: 'Remember me',
72
+ },
73
+ submit: {
74
+ type: 'submit',
75
+ label: 'Sign In',
76
+ },
77
+ },
78
+ } satisfies FormConfig;
79
+ ```
80
+
81
+ ### 2. Choose Your Framework Adapter
82
+
83
+ #### Next.js (App Router)
84
+
85
+ ```typescript
86
+ // app/login/page.tsx
87
+ 'use client';
88
+
89
+ import { NextForm } from 'performa/nextjs';
90
+ import { loginForm } from './form-config';
91
+
92
+ export default function LoginPage() {
93
+ return (
94
+ <NextForm
95
+ config={loginForm}
96
+ action={submitLogin}
97
+ onSuccess={(data) => {
98
+ console.log('Login successful', data);
99
+ }}
100
+ />
101
+ );
102
+ }
103
+ ```
104
+
105
+ ```typescript
106
+ // app/login/actions.ts
107
+ 'use server';
108
+
109
+ import { validateForm } from 'performa/server';
110
+ import { loginForm } from './form-config';
111
+
112
+ export async function submitLogin(prevState: any, formData: FormData) {
113
+ const result = await validateForm(
114
+ new Request('', { method: 'POST', body: formData }),
115
+ loginForm
116
+ );
117
+
118
+ if (result.hasErrors) {
119
+ return { errors: result.errors };
120
+ }
121
+
122
+ // Process login with result.values
123
+ const { email, password } = result.values;
124
+
125
+ // Your authentication logic here
126
+
127
+ return { success: true };
128
+ }
129
+ ```
130
+
131
+ #### React Router
132
+
133
+ ```typescript
134
+ // routes/login.tsx
135
+ import { ReactRouterForm } from 'performa/react-router';
136
+ import { loginForm } from './form-config';
137
+
138
+ export default function LoginRoute() {
139
+ return <ReactRouterForm config={loginForm} />;
140
+ }
141
+
142
+ // Action handler
143
+ import { validateForm } from 'formbase/server';
144
+
145
+ export async function action({ request }: ActionFunctionArgs) {
146
+ const result = await validateForm(request, loginForm);
147
+
148
+ if (result.hasErrors) {
149
+ return { errors: result.errors };
150
+ }
151
+
152
+ // Process login
153
+ return redirect('/dashboard');
154
+ }
155
+ ```
156
+
157
+ #### TanStack Start
158
+
159
+ ```typescript
160
+ import { TanStackStartForm } from 'performa/tanstack-start';
161
+ import { createServerFn } from '@tanstack/start';
162
+ import { validateForm } from 'formbase/server';
163
+
164
+ const submitLogin = createServerFn({ method: 'POST' }).handler(
165
+ async ({ data }: { data: FormData }) => {
166
+ const result = await validateForm(
167
+ new Request('', { method: 'POST', body: data }),
168
+ loginForm
169
+ );
170
+
171
+ if (result.hasErrors) {
172
+ return { errors: result.errors };
173
+ }
174
+
175
+ return { success: true };
176
+ }
177
+ );
178
+
179
+ export default function LoginPage() {
180
+ return <TanStackStartForm config={loginForm} action={submitLogin} />;
181
+ }
182
+ ```
183
+
184
+ ## Form Configuration
185
+
186
+ ### Field Types
187
+
188
+ formbase supports the following field types:
189
+
190
+ #### Text Inputs
191
+ - `text` - Standard text input
192
+ - `email` - Email input with validation
193
+ - `password` - Password input with masking
194
+ - `url` - URL input
195
+ - `tel` - Telephone number input
196
+ - `number` - Numeric input
197
+ - `date` - Date picker
198
+ - `time` - Time picker
199
+
200
+ #### Textareas
201
+ - `textarea` - Multi-line text input
202
+
203
+ #### Select Inputs
204
+ - `select` - Dropdown selection with options
205
+
206
+ #### Radio Inputs
207
+ - `radio` - Radio button group
208
+
209
+ #### Checkboxes and Toggles
210
+ - `checkbox` - Single checkbox
211
+ - `toggle` - Toggle switch
212
+
213
+ #### File Inputs
214
+ - `file` - File upload with validation and preview
215
+
216
+ #### Special Types
217
+ - `datetime` - Combined date and time picker
218
+ - `hidden` - Hidden input field
219
+ - `none` - Display-only field (no input)
220
+ - `submit` - Submit button
221
+
222
+ ### Validation Rules
223
+
224
+ All validation rules are applied server-side for security:
225
+
226
+ ```typescript
227
+ {
228
+ rules: {
229
+ required: true, // Field is required
230
+ minLength: 8, // Minimum character length
231
+ maxLength: 100, // Maximum character length
232
+ pattern: /^[A-Z0-9]+$/, // Custom regex pattern
233
+ matches: 'password', // Must match another field
234
+ isEmail: true, // Valid email format
235
+ isUrl: true, // Valid URL format
236
+ isPhone: true, // Valid phone number
237
+ isDate: true, // Valid date
238
+ isTime: true, // Valid time
239
+ isNumber: true, // Valid number
240
+ isInteger: true, // Valid integer
241
+ isAlphanumeric: true, // Only letters and numbers
242
+ isSlug: true, // Valid URL slug
243
+ isUUID: true, // Valid UUID
244
+ denyHtml: true, // Reject HTML tags
245
+ weakPasswordCheck: true, // Check against known breached passwords
246
+ minValue: 0, // Minimum numeric value
247
+ maxValue: 100, // Maximum numeric value
248
+ mustBeEither: ['admin', 'user'], // Value must be one of the options
249
+ mimeTypes: ['JPEG', 'PNG', 'PDF'], // Allowed file types
250
+ maxFileSize: 5 * 1024 * 1024, // Maximum file size in bytes
251
+ }
252
+ }
253
+ ```
254
+
255
+ ### Field Configuration Options
256
+
257
+ ```typescript
258
+ {
259
+ type: 'text', // Field type (required)
260
+ label: 'Username', // Field label (required)
261
+ placeholder: 'Enter username', // Placeholder text
262
+ defaultValue: '', // Default value
263
+ defaultChecked: false, // Default checked state (checkbox/toggle)
264
+ disabled: false, // Disable the field
265
+ className: 'custom-class', // Additional CSS classes
266
+ before: 'Help text above', // Content before the field
267
+ beforeClassName: 'text-sm', // Classes for before content
268
+ after: 'Help text below', // Content after the field
269
+ afterClassName: 'text-sm', // Classes for after content
270
+ uploadDir: 'avatars', // Upload directory for files
271
+ width: 'full', // Field width (full, half, third, quarter)
272
+ options: [ // Options for select/radio
273
+ { value: 'opt1', label: 'Option 1' },
274
+ { value: 'opt2', label: 'Option 2' },
275
+ ],
276
+ rules: { /* validation rules */ },
277
+ }
278
+ ```
279
+
280
+ ## Server-Side Validation
281
+
282
+ ### Validation Function
283
+
284
+ The `validateForm` function performs server-side validation and returns typed results:
285
+
286
+ ```typescript
287
+ import { validateForm } from 'formbase/server';
288
+
289
+ const result = await validateForm(request, formConfig);
290
+
291
+ if (result.hasErrors) {
292
+ // result.errors contains field-specific error messages
293
+ return { errors: result.errors };
294
+ }
295
+
296
+ // result.values contains validated and typed form data
297
+ const { email, password } = result.values;
298
+ ```
299
+
300
+ ### Custom Error Messages
301
+
302
+ You can customize validation error messages:
303
+
304
+ ```typescript
305
+ import { defaultErrorMessages } from 'performa/server';
306
+
307
+ // Customize individual messages
308
+ defaultErrorMessages.required = (label) => `${label} is mandatory`;
309
+ defaultErrorMessages.minLength = (label, min) =>
310
+ `${label} needs at least ${min} characters`;
311
+ ```
312
+
313
+ ### Return Type
314
+
315
+ errors: {
316
+ fieldName?: string; // Error message for each field
317
+ __server?: string; // Server-level error message
318
+ } | undefined;
319
+ values: {
320
+ fieldName: string | boolean | File; // Validated values
321
+ };
322
+ hasErrors: boolean; // Quick check for validation failure
323
+ }
324
+ ```
325
+
326
+ ## File Uploads
327
+
328
+ ### Configuration
329
+
330
+ ```typescript
331
+ {
332
+ avatar: {
333
+ type: 'file',
334
+ label: 'Profile Picture',
335
+ rules: {
336
+ required: true,
337
+ mimeTypes: ['JPEG', 'PNG', 'WEBP'],
338
+ maxFileSize: 2 * 1024 * 1024, // 2MB
339
+ },
340
+ uploadDir: 'avatars', // Optional: subdirectory for uploads
341
+ }
342
+ }
343
+ ```
344
+
345
+ ### Supported MIME Types
346
+
347
+ - Images: `JPEG`, `PNG`, `GIF`, `WEBP`, `SVG`, `BMP`, `TIFF`
348
+ - Documents: `PDF`, `DOC`, `DOCX`, `XLS`, `XLSX`, `PPT`, `PPTX`, `TXT`, `CSV`
349
+ - Archives: `ZIP`, `RAR`, `TAR`, `GZIP`
350
+ - Media: `MP3`, `MP4`, `WAV`, `AVI`, `MOV`
351
+
352
+ ### File Preview
353
+
354
+ Files are automatically previewed if:
355
+ - A `baseUrl` prop is provided to the form component
356
+ - The file is an image type
357
+ - A `defaultValue` exists (for editing forms)
358
+
359
+ ```typescript
360
+ <NextForm
361
+ config={formConfig}
362
+ action={submitForm}
363
+ fileBaseUrl="https://cdn.example.com"
364
+ />
365
+ ```
366
+
367
+ ### Handling File Uploads
368
+
369
+ ```typescript
370
+ export async function submitForm(prevState: any, formData: FormData) {
371
+ const result = await validateForm(
372
+ new Request('', { method: 'POST', body: formData }),
373
+ formConfig
374
+ );
375
+
376
+ if (result.hasErrors) {
377
+ return { errors: result.errors };
378
+ }
379
+
380
+ const file = result.values.avatar as File;
381
+
382
+ // Upload file to storage
383
+ const buffer = Buffer.from(await file.arrayBuffer());
384
+ const filename = `${Date.now()}-${file.name}`;
385
+
386
+ // Save to file system, S3, etc.
387
+ await saveFile(filename, buffer);
388
+
389
+ return { success: true };
390
+ }
391
+ ```
392
+
393
+ ## Theming
394
+
395
+ ### Default Theme
396
+
397
+ formbase comes with a default theme optimized for Tailwind CSS with dark mode support:
398
+
399
+ ```typescript
400
+ import { FormThemeProvider } from 'performa';
401
+
402
+ function App() {
403
+ return (
404
+ <FormThemeProvider>
405
+ {/* Your forms here */}
406
+ </FormThemeProvider>
407
+ );
408
+ }
409
+ ```
410
+
411
+ ### Custom Theme
412
+
413
+ Override the default theme with your own styles:
414
+
415
+ ```typescript
416
+ import { FormThemeProvider } from 'formbase';
417
+
418
+ const customTheme = {
419
+ formGroup: 'mb-6',
420
+ label: {
421
+ base: 'block text-sm font-semibold mb-2',
422
+ required: 'text-red-600 ml-1',
423
+ },
424
+ input: {
425
+ base: 'w-full px-4 py-3 border rounded-lg focus:ring-2',
426
+ error: 'border-red-500 focus:ring-red-500',
427
+ },
428
+ error: 'text-red-600 text-sm mt-2',
429
+ button: {
430
+ primary: 'bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700',
431
+ },
432
+ };
433
+
434
+ function App() {
435
+ return (
436
+ <FormThemeProvider theme={customTheme}>
437
+ {/* Your forms here */}
438
+ </FormThemeProvider>
439
+ );
440
+ }
441
+ ```
442
+
443
+ ### Theme Structure
444
+
445
+ The complete theme object structure:
446
+
447
+ ```typescript
448
+ {
449
+ form: string; // Form element styles
450
+ formGroup: string; // Field group wrapper
451
+ fieldset: string; // Fieldset styles
452
+ label: {
453
+ base: string; // Label base styles
454
+ required: string; // Required asterisk styles
455
+ };
456
+ input: {
457
+ base: string; // Input base styles
458
+ error: string; // Error state styles
459
+ };
460
+ textarea: {
461
+ base: string;
462
+ error: string;
463
+ };
464
+ select: {
465
+ base: string;
466
+ error: string;
467
+ };
468
+ checkbox: {
469
+ base: string;
470
+ label: string;
471
+ error: string;
472
+ };
473
+ radio: {
474
+ group: string;
475
+ base: string;
476
+ label: string;
477
+ };
478
+ toggle: {
479
+ wrapper: string;
480
+ base: string;
481
+ slider: string;
482
+ label: string;
483
+ };
484
+ file: {
485
+ dropzone: string;
486
+ dropzoneActive: string;
487
+ dropzoneError: string;
488
+ icon: string;
489
+ text: string;
490
+ hint: string;
491
+ };
492
+ datetime: {
493
+ input: string;
494
+ iconButton: string;
495
+ dropdown: string;
496
+ navButton: string;
497
+ monthYear: string;
498
+ weekday: string;
499
+ day: string;
500
+ daySelected: string;
501
+ timeLabel: string;
502
+ timeInput: string;
503
+ formatButton: string;
504
+ periodButton: string;
505
+ periodButtonActive: string;
506
+ };
507
+ button: {
508
+ primary: string;
509
+ secondary: string;
510
+ };
511
+ alert: {
512
+ base: string;
513
+ error: string;
514
+ success: string;
515
+ warning: string;
516
+ info: string;
517
+ };
518
+ error: string;
519
+ }
520
+ ```
521
+
522
+ ### Custom Labels
523
+
524
+ Customize form labels and messages:
525
+
526
+ ```typescript
527
+ import { FormThemeProvider } from 'formbase';
528
+
529
+ const customLabels = {
530
+ fileUpload: {
531
+ clickToUpload: 'Choose file',
532
+ dragAndDrop: 'or drag and drop',
533
+ allowedTypes: 'Supported formats:',
534
+ maxSize: 'Maximum size:',
535
+ },
536
+ datetime: {
537
+ weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
538
+ clear: 'Clear',
539
+ done: 'Done',
540
+ timeFormat: 'Time Format',
541
+ },
542
+ };
543
+
544
+ function App() {
545
+ return (
546
+ <FormThemeProvider labels={customLabels}>
547
+ {/* Your forms here */}
548
+ </FormThemeProvider>
549
+ );
550
+ }
551
+ ```
552
+
553
+ ## Advanced Usage
554
+
555
+ ### Using Hooks for Custom Forms
556
+
557
+ #### Next.js
558
+
559
+ ```typescript
560
+ import { useNextForm } from 'performa/nextjs';
561
+
562
+ function CustomForm() {
563
+ const { formAction, errors, isPending, isSuccess } = useNextForm(
564
+ submitForm,
565
+ (data) => {
566
+ console.log('Success!', data);
567
+ }
568
+ );
569
+
570
+ return (
571
+ <form action={formAction}>
572
+ <input name="email" type="email" />
573
+ {errors?.email && <p>{errors.email}</p>}
574
+ <button disabled={isPending}>
575
+ {isPending ? 'Submitting...' : 'Submit'}
576
+ </button>
577
+ {isSuccess && <p>Form submitted successfully!</p>}
578
+ </form>
579
+ );
580
+ }
581
+ ```
582
+
583
+ #### React Router
584
+
585
+ ```typescript
586
+ import { useReactRouterForm } from 'performa/react-router';
587
+
588
+ function CustomForm() {
589
+ const { fetcher, errors, isSubmitting } = useReactRouterForm('my-form');
590
+
591
+ return (
592
+ <fetcher.Form method="post">
593
+ <input name="email" type="email" />
594
+ {errors?.email && <p>{errors.email}</p>}
595
+ <button disabled={isSubmitting}>Submit</button>
596
+ </fetcher.Form>
597
+ );
598
+ }
599
+ ```
600
+
601
+ #### TanStack Start
602
+
603
+ ```typescript
604
+ import { useTanStackStartForm } from 'performa/tanstack-start';
605
+
606
+ function CustomForm() {
607
+ const { handleSubmit, errors, isPending } = useTanStackStartForm(
608
+ submitForm
609
+ );
610
+
611
+ return (
612
+ <form onSubmit={handleSubmit}>
613
+ <input name="email" type="email" />
614
+ {errors?.email && <p>{errors.email}</p>}
615
+ <button disabled={isPending}>Submit</button>
616
+ </form>
617
+ );
618
+ }
619
+ ```
620
+
621
+ ### Conditional Fields
622
+
623
+ ```typescript
624
+ const formConfig: FormConfig = {
625
+ key: 'signup',
626
+ fields: {
627
+ accountType: {
628
+ type: 'select',
629
+ label: 'Account Type',
630
+ options: [
631
+ { value: 'personal', label: 'Personal' },
632
+ { value: 'business', label: 'Business' },
633
+ ],
634
+ rules: { required: true },
635
+ },
636
+ // Only show company name for business accounts
637
+ ...(accountType === 'business' && {
638
+ companyName: {
639
+ type: 'text',
640
+ label: 'Company Name',
641
+ rules: { required: true },
642
+ },
643
+ }),
644
+ },
645
+ };
646
+ ```
647
+
648
+ ### Multi-Step Forms
649
+
650
+ ```typescript
651
+ const step1Config: FormConfig = {
652
+ key: 'registration-step1',
653
+ fields: {
654
+ email: { type: 'email', label: 'Email', rules: { required: true } },
655
+ password: { type: 'password', label: 'Password', rules: { required: true } },
656
+ submit: { type: 'submit', label: 'Continue' },
657
+ },
658
+ };
659
+
660
+ const step2Config: FormConfig = {
661
+ key: 'registration-step2',
662
+ fields: {
663
+ firstName: { type: 'text', label: 'First Name', rules: { required: true } },
664
+ lastName: { type: 'text', label: 'Last Name', rules: { required: true } },
665
+ submit: { type: 'submit', label: 'Complete Registration' },
666
+ },
667
+ };
668
+
669
+ function MultiStepForm() {
670
+ const [step, setStep] = useState(1);
671
+
672
+ return (
673
+ <>
674
+ {step === 1 && (
675
+ <NextForm
676
+ config={step1Config}
677
+ action={submitStep1}
678
+ onSuccess={() => setStep(2)}
679
+ />
680
+ )}
681
+ {step === 2 && (
682
+ <NextForm
683
+ config={step2Config}
684
+ action={submitStep2}
685
+ />
686
+ )}
687
+ </>
688
+ );
689
+ }
690
+ ```
691
+
692
+ ### Dynamic Field Options
693
+
694
+ ```typescript
695
+ function DynamicForm() {
696
+ const [categories, setCategories] = useState([]);
697
+
698
+ useEffect(() => {
699
+ fetch('/api/categories')
700
+ .then(res => res.json())
701
+ .then(data => setCategories(data));
702
+ }, []);
703
+
704
+ const formConfig: FormConfig = {
705
+ key: 'product',
706
+ fields: {
707
+ category: {
708
+ type: 'select',
709
+ label: 'Category',
710
+ options: categories.map(cat => ({
711
+ value: cat.id,
712
+ label: cat.name,
713
+ })),
714
+ rules: { required: true },
715
+ },
716
+ },
717
+ };
718
+
719
+ return <NextForm config={formConfig} action={submitProduct} />;
720
+ }
721
+ ```
722
+
723
+ ## TypeScript
724
+
725
+ formbase is built with TypeScript and provides full type safety:
726
+
727
+ ```typescript
728
+ import { FormConfig } from 'performa';
729
+ import { validateForm } from 'performa/server';
730
+
731
+ // Type-safe form configuration using 'satisfies'
732
+ // This gives you both type checking AND proper inference
733
+ const formConfig = {
734
+ key: 'contact',
735
+ fields: {
736
+ name: {
737
+ type: 'text',
738
+ label: 'Name',
739
+ rules: { required: true },
740
+ },
741
+ email: {
742
+ type: 'email',
743
+ label: 'Email',
744
+ rules: { required: true, isEmail: true },
745
+ },
746
+ },
747
+ } satisfies FormConfig;
748
+
749
+ // Type-safe validation result
750
+ // TypeScript automatically infers the correct types from formConfig
751
+ const result = await validateForm(request, formConfig);
752
+
753
+ if (result.hasErrors) {
754
+ // result.errors is properly typed with field names
755
+ console.log(result.errors.name); // string | undefined
756
+ console.log(result.errors.email); // string | undefined
757
+ }
758
+
759
+ // result.values is properly typed based on field types
760
+ const name: string = result.values.name; // TypeScript knows this is a string
761
+ const email: string = result.values.email; // TypeScript knows this is a string
762
+ ```
763
+
764
+ ## Security Considerations
765
+
766
+ ### Server-Side Validation
767
+
768
+ All validation is performed server-side. Client-side HTML5 validation attributes are added for better UX, but should not be relied upon for security.
769
+
770
+ ### File Upload Security
771
+
772
+ 1. **MIME Type Validation**: Files are validated by actual MIME type, not just extension
773
+ 2. **Size Limits**: Enforce maximum file sizes to prevent DoS attacks
774
+ 3. **File Storage**: Always validate and sanitize filenames before storage
775
+ 4. **Virus Scanning**: Implement virus scanning for uploaded files in production
776
+
777
+ ### XSS Prevention
778
+
779
+ - All form inputs are properly escaped when rendered
780
+ - The `denyHtml` validation rule prevents HTML injection
781
+ - Use the `pattern` rule to restrict input to safe characters
782
+
783
+ ### CSRF Protection
784
+
785
+ Implement CSRF protection at the framework level:
786
+
787
+ ```typescript
788
+ // Next.js example with csrf-token
789
+ import { headers } from 'next/headers';
790
+
791
+ export async function submitForm(prevState: any, formData: FormData) {
792
+ const headersList = headers();
793
+ const csrfToken = headersList.get('x-csrf-token');
794
+
795
+ // Validate CSRF token
796
+
797
+ const result = await validateForm(request, formConfig);
798
+ // ...
799
+ }
800
+ ```
801
+
802
+ ## Performance
803
+
804
+ ### Bundle Sizes
805
+
806
+ - Core library: 7.69 KB (brotli)
807
+ - Server validation: 367 B (brotli)
808
+ - React Router adapter: 18.8 KB (brotli)
809
+ - Next.js adapter: 7.3 KB (brotli)
810
+ - TanStack Start adapter: 7.36 KB (brotli)
811
+
812
+ ### Optimization
813
+
814
+ All components are memoized with `React.memo` for optimal performance. The library uses:
815
+
816
+ - Tree-shaking for unused code elimination
817
+ - Code splitting between client and server modules
818
+ - Minimal dependencies (only `lucide-react` for icons)
819
+
820
+ ## Accessibility
821
+
822
+ formbase is built with accessibility in mind:
823
+
824
+ - Proper ARIA attributes on all form elements
825
+ - Keyboard navigation support
826
+ - Screen reader friendly error messages
827
+ - Focus management
828
+ - Semantic HTML structure
829
+ - High contrast mode support
830
+
831
+ ## Browser Support
832
+
833
+ - Chrome/Edge (latest)
834
+ - Firefox (latest)
835
+ - Safari (latest)
836
+ - iOS Safari (latest)
837
+ - Chrome Android (latest)
838
+
839
+ ## Contributing
840
+
841
+ Contributions are welcome! Please see our contributing guidelines.
842
+
843
+ ## License
844
+
845
+ MIT License - see LICENSE file for details
846
+
847
+ ## Support
848
+
849
+ - GitHub Issues: https://github.com/matttehat/performa/issues
850
+ - Documentation: https://github.com/matttehat/performa#readme