@zoyth/simple-site-framework 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/LICENSE +21 -0
- package/README.md +572 -0
- package/bin/create-simple-site.js +390 -0
- package/bin/simple-site.js +664 -0
- package/dist/client.js +135 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +107 -0
- package/dist/client.mjs.map +1 -0
- package/dist/components/index.d.mts +3936 -0
- package/dist/components/index.d.ts +3936 -0
- package/dist/components/index.js +38265 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +38173 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/config/index.d.mts +298 -0
- package/dist/config/index.d.ts +298 -0
- package/dist/config/index.js +19 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/index.mjs +1 -0
- package/dist/config/index.mjs.map +1 -0
- package/dist/index.d.mts +2184 -0
- package/dist/index.d.ts +2184 -0
- package/dist/index.js +1713 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1605 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib/i18n/index.js +665 -0
- package/dist/lib/i18n/index.js.map +1 -0
- package/dist/lib/i18n/index.mjs +621 -0
- package/dist/lib/i18n/index.mjs.map +1 -0
- package/docs/DOCUMENTATION-STRUCTURE.md +1156 -0
- package/docs/EXPORTS.md +125 -0
- package/docs/PERFORMANCE.md +757 -0
- package/docs/POLICY-PAGES.md +867 -0
- package/docs/ROADMAP.md +334 -0
- package/docs/SEO.md +455 -0
- package/docs/SITEMAP.md +708 -0
- package/docs/STRUCTURED-DATA.md +671 -0
- package/docs/accessibility/common-patterns.md +529 -0
- package/docs/accessibility/keyboard-navigation.md +263 -0
- package/docs/accessibility/overview.md +122 -0
- package/docs/accessibility/screen-readers.md +311 -0
- package/docs/accessibility/wcag-compliance.md +159 -0
- package/docs/api/README.md +164 -0
- package/docs/api/components/Accessibility.md +356 -0
- package/docs/api/components/Button.md +240 -0
- package/docs/api/components/HeroSection.md +306 -0
- package/docs/architecture/decisions.md +449 -0
- package/docs/components/AnalyticsTracker.md +58 -0
- package/docs/components/AnimatedCounter.md +48 -0
- package/docs/components/AnimatedSection.md +56 -0
- package/docs/components/BlogCard.md +42 -0
- package/docs/components/Checkbox.md +56 -0
- package/docs/components/CodeBlock.md +52 -0
- package/docs/components/ComparisonTable.md +40 -0
- package/docs/components/ComponentDemo.md +38 -0
- package/docs/components/CountdownTimer.md +51 -0
- package/docs/components/ExitIntentModal.md +56 -0
- package/docs/components/FAQAccordion.md +66 -0
- package/docs/components/FeaturesGrid.md +55 -0
- package/docs/components/FileUpload.md +54 -0
- package/docs/components/I18nMetaTags.md +55 -0
- package/docs/components/Icon.md +53 -0
- package/docs/components/LazySection.md +46 -0
- package/docs/components/LiveProof.md +53 -0
- package/docs/components/LoadingSpinner.md +46 -0
- package/docs/components/MultiStepForm.md +48 -0
- package/docs/components/PolicyLayout.md +55 -0
- package/docs/components/PricingTable.md +49 -0
- package/docs/components/Radio.md +59 -0
- package/docs/components/SEOMetaTags.md +58 -0
- package/docs/components/ScriptInjector.md +50 -0
- package/docs/components/Select.md +72 -0
- package/docs/components/Skeleton.md +47 -0
- package/docs/components/StatsSection.md +48 -0
- package/docs/components/StickyBar.md +62 -0
- package/docs/components/StructuredData.md +99 -0
- package/docs/components/StyleGuide.md +46 -0
- package/docs/components/TableOfContents.md +47 -0
- package/docs/components/TestimonialCarousel.md +42 -0
- package/docs/components/Timeline.md +51 -0
- package/docs/components/Toast.md +59 -0
- package/docs/components/TrackedLink.md +62 -0
- package/docs/components/TrustBadges.md +44 -0
- package/docs/components/conversion/MobileCTA.md +363 -0
- package/docs/components/forms/ContactForm.md +75 -0
- package/docs/components/forms/FormField.md +74 -0
- package/docs/components/layout/Footer.md +601 -0
- package/docs/components/layout/Header.md +549 -0
- package/docs/components/layout/LanguageSelector.md +54 -0
- package/docs/components/layout/LanguageSwitcher.md +24 -0
- package/docs/components/overview.md +447 -0
- package/docs/components/sections/AboutSection.md +48 -0
- package/docs/components/sections/CTASection.md +596 -0
- package/docs/components/sections/CaseStudySection.md +47 -0
- package/docs/components/sections/ContactSection.md +599 -0
- package/docs/components/sections/FeatureSection.md +44 -0
- package/docs/components/sections/HeroSection.md +404 -0
- package/docs/components/sections/LogosSection.md +47 -0
- package/docs/components/sections/PersonalTaxesSection.md +23 -0
- package/docs/components/sections/RecruitingSection.md +23 -0
- package/docs/components/sections/SecurePortalSection.md +23 -0
- package/docs/components/sections/ServicePageLayout.md +52 -0
- package/docs/components/sections/ServicesSection.md +49 -0
- package/docs/components/sections/TestimonialSection.md +44 -0
- package/docs/components/sections/WhyChooseUsSection.md +54 -0
- package/docs/components/ui/Breadcrumb.md +70 -0
- package/docs/components/ui/Button.md +514 -0
- package/docs/components/ui/Card.md +501 -0
- package/docs/components/ui/Input.md +54 -0
- package/docs/components/ui/MobileLinks.md +43 -0
- package/docs/components/ui/Modal.md +60 -0
- package/docs/components/ui/Tabs.md +62 -0
- package/docs/components/ui/Textarea.md +52 -0
- package/docs/core-concepts/configuration-driven.md +552 -0
- package/docs/core-concepts/overview.md +351 -0
- package/docs/features/accessibility/README.md +73 -0
- package/docs/features/accessibility/aria-support.md +177 -0
- package/docs/features/accessibility/color-contrast.md +155 -0
- package/docs/features/accessibility/focus-management.md +187 -0
- package/docs/features/accessibility/testing.md +196 -0
- package/docs/features/analytics/README.md +51 -0
- package/docs/features/analytics/ab-testing.md +171 -0
- package/docs/features/analytics/conversion-tracking.md +207 -0
- package/docs/features/analytics/custom-events.md +219 -0
- package/docs/features/analytics/privacy.md +198 -0
- package/docs/features/analytics/setup.md +114 -0
- package/docs/features/analytics/tracking-events.md +224 -0
- package/docs/features/i18n/README.md +51 -0
- package/docs/features/i18n/best-practices.md +273 -0
- package/docs/features/i18n/configuration.md +84 -0
- package/docs/features/i18n/formatting.md +133 -0
- package/docs/features/i18n/locale-detection.md +122 -0
- package/docs/features/i18n/routing.md +99 -0
- package/docs/features/i18n/rtl-support.md +191 -0
- package/docs/features/i18n/translations.md +129 -0
- package/docs/features/internationalization.md +595 -0
- package/docs/features/performance/README.md +77 -0
- package/docs/features/performance/bundle-size.md +134 -0
- package/docs/features/performance/caching.md +131 -0
- package/docs/features/performance/code-splitting.md +121 -0
- package/docs/features/performance/image-optimization.md +110 -0
- package/docs/features/performance/lazy-loading.md +92 -0
- package/docs/features/performance/monitoring.md +148 -0
- package/docs/features/seo/README.md +51 -0
- package/docs/features/seo/best-practices.md +184 -0
- package/docs/features/seo/canonical-urls.md +182 -0
- package/docs/features/seo/meta-tags.md +126 -0
- package/docs/features/seo/open-graph.md +166 -0
- package/docs/features/seo/robots-txt.md +146 -0
- package/docs/features/seo/sitemaps.md +162 -0
- package/docs/features/seo/structured-data.md +166 -0
- package/docs/getting-started/installation.md +292 -0
- package/docs/getting-started/introduction.md +195 -0
- package/docs/getting-started/quick-start.md +460 -0
- package/docs/guides/analytics-setup.md +616 -0
- package/docs/i18n/CONFIGURATION.md +353 -0
- package/docs/i18n/EXAMPLES.md +402 -0
- package/docs/i18n/MIGRATION.md +260 -0
- package/docs/i18n/SEO.md +392 -0
- package/docs/i18n/STATIC-GENERATION-FIX.md +71 -0
- package/docs/migration/changelog.md +136 -0
- package/docs/migration/overview.md +233 -0
- package/docs/recipes/adding-animations.md +475 -0
- package/docs/recipes/forms-with-validation.md +393 -0
- package/package.json +152 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# Recipe: Forms with Validation
|
|
2
|
+
|
|
3
|
+
Complete guide to building accessible forms with validation using React Hook Form and Zod.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This recipe shows how to:
|
|
8
|
+
- Create forms with React Hook Form
|
|
9
|
+
- Validate with Zod schemas
|
|
10
|
+
- Display error messages accessibly
|
|
11
|
+
- Handle loading and success states
|
|
12
|
+
- Announce form submission results
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install react-hook-form @hookform/resolvers zod
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Basic Contact Form
|
|
21
|
+
|
|
22
|
+
### 1. Define Zod Schema
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { z } from 'zod'
|
|
26
|
+
|
|
27
|
+
const contactSchema = z.object({
|
|
28
|
+
name: z.string().min(2, 'Name must be at least 2 characters'),
|
|
29
|
+
email: z.string().email('Invalid email address'),
|
|
30
|
+
message: z.string().min(10, 'Message must be at least 10 characters'),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
type ContactFormData = z.infer<typeof contactSchema>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Create Form Component
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { useForm } from 'react-hook-form'
|
|
40
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
41
|
+
import { Button, FormField, useA11y } from '@zoyth/simple-site-framework'
|
|
42
|
+
import { useState } from 'react'
|
|
43
|
+
|
|
44
|
+
function ContactForm() {
|
|
45
|
+
const { announce } = useA11y()
|
|
46
|
+
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
47
|
+
const [isSuccess, setIsSuccess] = useState(false)
|
|
48
|
+
|
|
49
|
+
const {
|
|
50
|
+
register,
|
|
51
|
+
handleSubmit,
|
|
52
|
+
formState: { errors },
|
|
53
|
+
reset,
|
|
54
|
+
} = useForm<ContactFormData>({
|
|
55
|
+
resolver: zodResolver(contactSchema),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const onSubmit = async (data: ContactFormData) => {
|
|
59
|
+
setIsSubmitting(true)
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await fetch('/api/contact', {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
body: JSON.stringify(data),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
setIsSuccess(true)
|
|
69
|
+
announce('Message sent successfully!', 'polite')
|
|
70
|
+
reset()
|
|
71
|
+
|
|
72
|
+
setTimeout(() => setIsSuccess(false), 3000)
|
|
73
|
+
} catch (error) {
|
|
74
|
+
announce('Error sending message. Please try again.', 'assertive')
|
|
75
|
+
} finally {
|
|
76
|
+
setIsSubmitting(false)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
|
82
|
+
<FormField
|
|
83
|
+
name="name"
|
|
84
|
+
label="Name"
|
|
85
|
+
error={errors.name}
|
|
86
|
+
required
|
|
87
|
+
>
|
|
88
|
+
<input
|
|
89
|
+
{...register('name')}
|
|
90
|
+
type="text"
|
|
91
|
+
className="w-full px-4 py-2 border rounded"
|
|
92
|
+
aria-invalid={errors.name ? 'true' : 'false'}
|
|
93
|
+
/>
|
|
94
|
+
</FormField>
|
|
95
|
+
|
|
96
|
+
<FormField
|
|
97
|
+
name="email"
|
|
98
|
+
label="Email"
|
|
99
|
+
error={errors.email}
|
|
100
|
+
required
|
|
101
|
+
>
|
|
102
|
+
<input
|
|
103
|
+
{...register('email')}
|
|
104
|
+
type="email"
|
|
105
|
+
className="w-full px-4 py-2 border rounded"
|
|
106
|
+
aria-invalid={errors.email ? 'true' : 'false'}
|
|
107
|
+
/>
|
|
108
|
+
</FormField>
|
|
109
|
+
|
|
110
|
+
<FormField
|
|
111
|
+
name="message"
|
|
112
|
+
label="Message"
|
|
113
|
+
error={errors.message}
|
|
114
|
+
required
|
|
115
|
+
>
|
|
116
|
+
<textarea
|
|
117
|
+
{...register('message')}
|
|
118
|
+
rows={4}
|
|
119
|
+
className="w-full px-4 py-2 border rounded"
|
|
120
|
+
aria-invalid={errors.message ? 'true' : 'false'}
|
|
121
|
+
/>
|
|
122
|
+
</FormField>
|
|
123
|
+
|
|
124
|
+
<Button
|
|
125
|
+
type="submit"
|
|
126
|
+
variant="filled"
|
|
127
|
+
size="lg"
|
|
128
|
+
loading={isSubmitting}
|
|
129
|
+
loadingText="Sending..."
|
|
130
|
+
success={isSuccess}
|
|
131
|
+
successText="Sent!"
|
|
132
|
+
disabled={isSubmitting}
|
|
133
|
+
>
|
|
134
|
+
Send Message
|
|
135
|
+
</Button>
|
|
136
|
+
</form>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Advanced: Multi-Step Form
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import { useState } from 'react'
|
|
145
|
+
import { useForm } from 'react-hook-form'
|
|
146
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
147
|
+
import { z } from 'zod'
|
|
148
|
+
import { Button, FormField } from '@zoyth/simple-site-framework'
|
|
149
|
+
|
|
150
|
+
// Step schemas
|
|
151
|
+
const step1Schema = z.object({
|
|
152
|
+
name: z.string().min(2),
|
|
153
|
+
email: z.string().email(),
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const step2Schema = z.object({
|
|
157
|
+
company: z.string().min(2),
|
|
158
|
+
role: z.string().min(2),
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const step3Schema = z.object({
|
|
162
|
+
message: z.string().min(10),
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Combined schema
|
|
166
|
+
const fullSchema = step1Schema.merge(step2Schema).merge(step3Schema)
|
|
167
|
+
type FormData = z.infer<typeof fullSchema>
|
|
168
|
+
|
|
169
|
+
function MultiStepForm() {
|
|
170
|
+
const [step, setStep] = useState(1)
|
|
171
|
+
|
|
172
|
+
const {
|
|
173
|
+
register,
|
|
174
|
+
handleSubmit,
|
|
175
|
+
formState: { errors },
|
|
176
|
+
trigger,
|
|
177
|
+
} = useForm<FormData>({
|
|
178
|
+
resolver: zodResolver(fullSchema),
|
|
179
|
+
mode: 'onChange',
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const nextStep = async () => {
|
|
183
|
+
let isValid = false
|
|
184
|
+
|
|
185
|
+
if (step === 1) {
|
|
186
|
+
isValid = await trigger(['name', 'email'])
|
|
187
|
+
} else if (step === 2) {
|
|
188
|
+
isValid = await trigger(['company', 'role'])
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (isValid) {
|
|
192
|
+
setStep(step + 1)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const onSubmit = async (data: FormData) => {
|
|
197
|
+
console.log('Form submitted:', data)
|
|
198
|
+
// Handle submission
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
203
|
+
{/* Progress indicator */}
|
|
204
|
+
<div className="mb-8">
|
|
205
|
+
<div className="flex justify-between mb-2">
|
|
206
|
+
{[1, 2, 3].map((s) => (
|
|
207
|
+
<div
|
|
208
|
+
key={s}
|
|
209
|
+
className={`w-1/3 h-2 ${
|
|
210
|
+
s <= step ? 'bg-primary' : 'bg-gray-200'
|
|
211
|
+
}`}
|
|
212
|
+
/>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
<p className="text-sm text-gray-600">
|
|
216
|
+
Step {step} of 3
|
|
217
|
+
</p>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
{/* Step 1: Personal Info */}
|
|
221
|
+
{step === 1 && (
|
|
222
|
+
<div className="space-y-6">
|
|
223
|
+
<FormField name="name" label="Name" error={errors.name} required>
|
|
224
|
+
<input {...register('name')} type="text" />
|
|
225
|
+
</FormField>
|
|
226
|
+
|
|
227
|
+
<FormField name="email" label="Email" error={errors.email} required>
|
|
228
|
+
<input {...register('email')} type="email" />
|
|
229
|
+
</FormField>
|
|
230
|
+
|
|
231
|
+
<Button onClick={nextStep} variant="filled">
|
|
232
|
+
Next
|
|
233
|
+
</Button>
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
{/* Step 2: Company Info */}
|
|
238
|
+
{step === 2 && (
|
|
239
|
+
<div className="space-y-6">
|
|
240
|
+
<FormField name="company" label="Company" error={errors.company} required>
|
|
241
|
+
<input {...register('company')} type="text" />
|
|
242
|
+
</FormField>
|
|
243
|
+
|
|
244
|
+
<FormField name="role" label="Role" error={errors.role} required>
|
|
245
|
+
<input {...register('role')} type="text" />
|
|
246
|
+
</FormField>
|
|
247
|
+
|
|
248
|
+
<div className="flex gap-4">
|
|
249
|
+
<Button onClick={() => setStep(1)} variant="outlined">
|
|
250
|
+
Back
|
|
251
|
+
</Button>
|
|
252
|
+
<Button onClick={nextStep} variant="filled">
|
|
253
|
+
Next
|
|
254
|
+
</Button>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
|
|
259
|
+
{/* Step 3: Message */}
|
|
260
|
+
{step === 3 && (
|
|
261
|
+
<div className="space-y-6">
|
|
262
|
+
<FormField name="message" label="Message" error={errors.message} required>
|
|
263
|
+
<textarea {...register('message')} rows={4} />
|
|
264
|
+
</FormField>
|
|
265
|
+
|
|
266
|
+
<div className="flex gap-4">
|
|
267
|
+
<Button onClick={() => setStep(2)} variant="outlined">
|
|
268
|
+
Back
|
|
269
|
+
</Button>
|
|
270
|
+
<Button type="submit" variant="filled">
|
|
271
|
+
Submit
|
|
272
|
+
</Button>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
</form>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Form with File Upload
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
import { useForm } from 'react-hook-form'
|
|
285
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
286
|
+
import { z } from 'zod'
|
|
287
|
+
|
|
288
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
|
|
289
|
+
const ACCEPTED_FILE_TYPES = ['image/jpeg', 'image/png', 'application/pdf']
|
|
290
|
+
|
|
291
|
+
const formSchema = z.object({
|
|
292
|
+
name: z.string().min(2),
|
|
293
|
+
file: z
|
|
294
|
+
.instanceof(FileList)
|
|
295
|
+
.refine((files) => files.length > 0, 'File is required')
|
|
296
|
+
.refine(
|
|
297
|
+
(files) => files[0]?.size <= MAX_FILE_SIZE,
|
|
298
|
+
'File size must be less than 5MB'
|
|
299
|
+
)
|
|
300
|
+
.refine(
|
|
301
|
+
(files) => ACCEPTED_FILE_TYPES.includes(files[0]?.type),
|
|
302
|
+
'Only .jpg, .png, and .pdf files are accepted'
|
|
303
|
+
),
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
function FileUploadForm() {
|
|
307
|
+
const {
|
|
308
|
+
register,
|
|
309
|
+
handleSubmit,
|
|
310
|
+
formState: { errors },
|
|
311
|
+
} = useForm({
|
|
312
|
+
resolver: zodResolver(formSchema),
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
const onSubmit = async (data) => {
|
|
316
|
+
const formData = new FormData()
|
|
317
|
+
formData.append('name', data.name)
|
|
318
|
+
formData.append('file', data.file[0])
|
|
319
|
+
|
|
320
|
+
await fetch('/api/upload', {
|
|
321
|
+
method: 'POST',
|
|
322
|
+
body: formData,
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
|
328
|
+
<FormField name="name" label="Name" error={errors.name}>
|
|
329
|
+
<input {...register('name')} type="text" />
|
|
330
|
+
</FormField>
|
|
331
|
+
|
|
332
|
+
<FormField
|
|
333
|
+
name="file"
|
|
334
|
+
label="Upload File"
|
|
335
|
+
error={errors.file}
|
|
336
|
+
helpText="Max 5MB. Accepted: JPG, PNG, PDF"
|
|
337
|
+
>
|
|
338
|
+
<input {...register('file')} type="file" accept=".jpg,.png,.pdf" />
|
|
339
|
+
</FormField>
|
|
340
|
+
|
|
341
|
+
<Button type="submit" variant="filled">
|
|
342
|
+
Upload
|
|
343
|
+
</Button>
|
|
344
|
+
</form>
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Accessibility Checklist
|
|
350
|
+
|
|
351
|
+
- [ ] All fields have associated labels
|
|
352
|
+
- [ ] Required fields marked with `required` prop
|
|
353
|
+
- [ ] Error messages are descriptive and specific
|
|
354
|
+
- [ ] Errors announced to screen readers
|
|
355
|
+
- [ ] Success announced to screen readers
|
|
356
|
+
- [ ] Loading state disables submit button
|
|
357
|
+
- [ ] Form can be completed with keyboard only
|
|
358
|
+
- [ ] Focus indicators are visible
|
|
359
|
+
- [ ] Error fields receive focus
|
|
360
|
+
- [ ] Help text provides clear guidance
|
|
361
|
+
|
|
362
|
+
## Best Practices
|
|
363
|
+
|
|
364
|
+
1. **Validation**
|
|
365
|
+
- Validate on client and server
|
|
366
|
+
- Provide instant feedback
|
|
367
|
+
- Show errors after blur, not while typing
|
|
368
|
+
- Clear errors when fixed
|
|
369
|
+
|
|
370
|
+
2. **Error Messages**
|
|
371
|
+
- Be specific about the problem
|
|
372
|
+
- Suggest how to fix it
|
|
373
|
+
- Use plain language
|
|
374
|
+
- Display near the field
|
|
375
|
+
|
|
376
|
+
3. **Loading States**
|
|
377
|
+
- Disable submit during submission
|
|
378
|
+
- Show loading indicator
|
|
379
|
+
- Don't lose form data on error
|
|
380
|
+
- Provide feedback on success
|
|
381
|
+
|
|
382
|
+
4. **Accessibility**
|
|
383
|
+
- Use semantic HTML (`<form>`, `<label>`, `<input>`)
|
|
384
|
+
- Link labels to inputs with `htmlFor`/`id`
|
|
385
|
+
- Use `aria-invalid` on error fields
|
|
386
|
+
- Announce success/error to screen readers
|
|
387
|
+
- Make form keyboard navigable
|
|
388
|
+
|
|
389
|
+
5. **UX**
|
|
390
|
+
- Auto-focus first field
|
|
391
|
+
- Save progress for long forms
|
|
392
|
+
- Confirm before leaving with unsaved changes
|
|
393
|
+
- Provide clear submission feedback
|
package/package.json
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zoyth/simple-site-framework",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A configuration-driven framework for building professional service websites",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"framework",
|
|
7
|
+
"nextjs",
|
|
8
|
+
"react",
|
|
9
|
+
"website-builder",
|
|
10
|
+
"professional-services"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/zoyth/simple-site-framework#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/zoyth/simple-site-framework/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/zoyth/simple-site-framework.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "François Lane",
|
|
22
|
+
"type": "commonjs",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.mjs",
|
|
27
|
+
"require": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./components": {
|
|
30
|
+
"types": "./dist/components/index.d.ts",
|
|
31
|
+
"import": "./dist/components/index.mjs",
|
|
32
|
+
"require": "./dist/components/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./config": {
|
|
35
|
+
"types": "./dist/config/index.d.ts",
|
|
36
|
+
"import": "./dist/config/index.mjs",
|
|
37
|
+
"require": "./dist/config/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./lib/i18n": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"import": "./dist/lib/i18n/index.mjs",
|
|
42
|
+
"require": "./dist/lib/i18n/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./client": {
|
|
45
|
+
"types": "./dist/client.d.ts",
|
|
46
|
+
"import": "./dist/client.mjs",
|
|
47
|
+
"require": "./dist/client.js"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"main": "./dist/index.js",
|
|
51
|
+
"types": "./dist/index.d.ts",
|
|
52
|
+
"bin": {
|
|
53
|
+
"create-simple-site": "bin/create-simple-site.js",
|
|
54
|
+
"simple-site": "bin/simple-site.js"
|
|
55
|
+
},
|
|
56
|
+
"directories": {
|
|
57
|
+
"doc": "docs",
|
|
58
|
+
"example": "examples"
|
|
59
|
+
},
|
|
60
|
+
"files": [
|
|
61
|
+
"dist",
|
|
62
|
+
"bin",
|
|
63
|
+
"docs"
|
|
64
|
+
],
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "tsup",
|
|
67
|
+
"dev": "tsup --watch",
|
|
68
|
+
"typecheck": "tsc --noEmit",
|
|
69
|
+
"test": "vitest",
|
|
70
|
+
"test:ui": "vitest --ui",
|
|
71
|
+
"test:coverage": "vitest --coverage",
|
|
72
|
+
"lint": "eslint src/",
|
|
73
|
+
"lint:fix": "eslint src/ --fix",
|
|
74
|
+
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
75
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
|
|
76
|
+
"prepare": "npm run build"
|
|
77
|
+
},
|
|
78
|
+
"dependencies": {
|
|
79
|
+
"clsx": "^2.1.1",
|
|
80
|
+
"rehype-slug": "^6.0.0",
|
|
81
|
+
"tailwind-merge": "^3.4.0"
|
|
82
|
+
},
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@eslint/js": "^9.39.2",
|
|
85
|
+
"@hookform/resolvers": "^3.9.1",
|
|
86
|
+
"@radix-ui/react-accordion": "^1.2.2",
|
|
87
|
+
"@radix-ui/react-dialog": "^1.1.4",
|
|
88
|
+
"@radix-ui/react-select": "^2.2.6",
|
|
89
|
+
"@radix-ui/react-tabs": "^1.1.13",
|
|
90
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
91
|
+
"@tailwindcss/typography": "^0.5.16",
|
|
92
|
+
"@testing-library/dom": "^10.4.1",
|
|
93
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
94
|
+
"@testing-library/react": "^16.3.2",
|
|
95
|
+
"@testing-library/user-event": "^14.6.1",
|
|
96
|
+
"@types/fs-extra": "^11.0.4",
|
|
97
|
+
"@types/inquirer": "^9.0.9",
|
|
98
|
+
"@types/node": "^20.11.5",
|
|
99
|
+
"@types/react": "^18.2.48",
|
|
100
|
+
"@types/react-dom": "^18.2.18",
|
|
101
|
+
"@types/react-syntax-highlighter": "^15.5.13",
|
|
102
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
103
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
104
|
+
"@vitejs/plugin-react": "^5.1.2",
|
|
105
|
+
"chalk": "^5.6.2",
|
|
106
|
+
"commander": "^14.0.3",
|
|
107
|
+
"eslint": "^9.39.2",
|
|
108
|
+
"eslint-config-prettier": "^10.1.8",
|
|
109
|
+
"eslint-plugin-react": "^7.37.5",
|
|
110
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
111
|
+
"framer-motion": "^11.15.0",
|
|
112
|
+
"fs-extra": "^11.3.3",
|
|
113
|
+
"inquirer": "^13.2.2",
|
|
114
|
+
"jsdom": "^27.4.0",
|
|
115
|
+
"lucide-react": "^0.563.0",
|
|
116
|
+
"next": "^15.0.0",
|
|
117
|
+
"next-mdx-remote": "^5.0.0",
|
|
118
|
+
"ora": "^9.1.0",
|
|
119
|
+
"prettier": "^3.8.1",
|
|
120
|
+
"react": "^19.0.0",
|
|
121
|
+
"react-dom": "^19.0.0",
|
|
122
|
+
"react-hook-form": "^7.54.2",
|
|
123
|
+
"react-syntax-highlighter": "^16.1.0",
|
|
124
|
+
"tsup": "^8.0.1",
|
|
125
|
+
"typescript": "^5.3.3",
|
|
126
|
+
"vitest": "^4.0.18",
|
|
127
|
+
"zod": "^3.24.1"
|
|
128
|
+
},
|
|
129
|
+
"peerDependencies": {
|
|
130
|
+
"@hookform/resolvers": "^3.3.0",
|
|
131
|
+
"@radix-ui/react-accordion": "^1.1.0",
|
|
132
|
+
"@radix-ui/react-dialog": "^1.0.0",
|
|
133
|
+
"@radix-ui/react-select": "^2.0.0",
|
|
134
|
+
"@radix-ui/react-tabs": "^1.1.0",
|
|
135
|
+
"@radix-ui/react-tooltip": "^1.0.0",
|
|
136
|
+
"@tailwindcss/typography": "^0.5.0",
|
|
137
|
+
"framer-motion": "^11.0.0",
|
|
138
|
+
"lucide-react": "^0.300.0",
|
|
139
|
+
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
140
|
+
"next-mdx-remote": "^5.0.0",
|
|
141
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
142
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
143
|
+
"react-hook-form": "^7.50.0",
|
|
144
|
+
"tailwindcss": "^3.4.0 || ^4.0.0",
|
|
145
|
+
"zod": "^3.22.0"
|
|
146
|
+
},
|
|
147
|
+
"publishConfig": {
|
|
148
|
+
"access": "public",
|
|
149
|
+
"registry": "https://registry.npmjs.org/"
|
|
150
|
+
},
|
|
151
|
+
"module": "./dist/index.mjs"
|
|
152
|
+
}
|