@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,529 @@
|
|
|
1
|
+
# Common Accessibility Patterns
|
|
2
|
+
|
|
3
|
+
Reusable accessibility patterns and code examples for common scenarios.
|
|
4
|
+
|
|
5
|
+
## Skip Links
|
|
6
|
+
|
|
7
|
+
Allow keyboard users to skip repetitive navigation.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { SkipLink } from '@zoyth/simple-site-framework'
|
|
11
|
+
|
|
12
|
+
export default function Layout({ children }) {
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<SkipLink href="#main">Skip to main content</SkipLink>
|
|
16
|
+
<SkipLink href="#nav">Skip to navigation</SkipLink>
|
|
17
|
+
|
|
18
|
+
<header>
|
|
19
|
+
<nav id="nav">{/* Navigation */}</nav>
|
|
20
|
+
</header>
|
|
21
|
+
|
|
22
|
+
<main id="main">{children}</main>
|
|
23
|
+
</>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Benefits:**
|
|
29
|
+
- Keyboard users can bypass repeated content
|
|
30
|
+
- Required for WCAG 2.4.1 (Level A)
|
|
31
|
+
- Hidden until focused
|
|
32
|
+
|
|
33
|
+
## Screen Reader Announcements
|
|
34
|
+
|
|
35
|
+
Notify users of dynamic content changes.
|
|
36
|
+
|
|
37
|
+
### Using useA11y Hook
|
|
38
|
+
```tsx
|
|
39
|
+
import { useA11y } from '@zoyth/simple-site-framework'
|
|
40
|
+
|
|
41
|
+
function FormExample() {
|
|
42
|
+
const { announce } = useA11y()
|
|
43
|
+
|
|
44
|
+
const handleSubmit = async (data) => {
|
|
45
|
+
try {
|
|
46
|
+
await submitForm(data)
|
|
47
|
+
announce('Form submitted successfully')
|
|
48
|
+
} catch (error) {
|
|
49
|
+
announce('Error submitting form. Please try again.', 'assertive')
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return <form onSubmit={handleSubmit}>{/* fields */}</form>
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Using A11yAnnouncer Component
|
|
58
|
+
```tsx
|
|
59
|
+
import { A11yAnnouncer } from '@zoyth/simple-site-framework'
|
|
60
|
+
|
|
61
|
+
function LoadingExample() {
|
|
62
|
+
const [status, setStatus] = useState('')
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
loadData().then(() => setStatus('Loading complete'))
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<div>{/* content */}</div>
|
|
71
|
+
<A11yAnnouncer message={status} politeness="polite" />
|
|
72
|
+
</>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Focus Management
|
|
78
|
+
|
|
79
|
+
### Focus Trap (Modals)
|
|
80
|
+
Keep focus inside modal while open:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { useFocusTrap } from '@zoyth/simple-site-framework'
|
|
84
|
+
|
|
85
|
+
function CustomModal({ isOpen, onClose }) {
|
|
86
|
+
const trapRef = useFocusTrap(isOpen)
|
|
87
|
+
|
|
88
|
+
if (!isOpen) return null
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
ref={trapRef}
|
|
93
|
+
role="dialog"
|
|
94
|
+
aria-modal="true"
|
|
95
|
+
aria-labelledby="modal-title"
|
|
96
|
+
>
|
|
97
|
+
<h2 id="modal-title">Confirm Action</h2>
|
|
98
|
+
<p>Are you sure?</p>
|
|
99
|
+
<Button onClick={onClose}>Cancel</Button>
|
|
100
|
+
<Button onClick={handleConfirm}>Confirm</Button>
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Focus Return
|
|
107
|
+
Return focus after closing modal:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { useFocusReturn } from '@zoyth/simple-site-framework'
|
|
111
|
+
|
|
112
|
+
function Modal({ children }) {
|
|
113
|
+
const returnRef = useFocusReturn()
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div ref={returnRef} role="dialog">
|
|
117
|
+
{children}
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Manual Focus Management
|
|
124
|
+
```tsx
|
|
125
|
+
import { useRef, useEffect } from 'react'
|
|
126
|
+
|
|
127
|
+
function SearchModal({ isOpen }) {
|
|
128
|
+
const inputRef = useRef(null)
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (isOpen && inputRef.current) {
|
|
132
|
+
inputRef.current.focus()
|
|
133
|
+
}
|
|
134
|
+
}, [isOpen])
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Modal isOpen={isOpen}>
|
|
138
|
+
<input ref={inputRef} type="search" />
|
|
139
|
+
</Modal>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Form Accessibility
|
|
145
|
+
|
|
146
|
+
### Basic Form
|
|
147
|
+
```tsx
|
|
148
|
+
import { FormField } from '@zoyth/simple-site-framework'
|
|
149
|
+
|
|
150
|
+
function ContactForm() {
|
|
151
|
+
return (
|
|
152
|
+
<form>
|
|
153
|
+
<FormField
|
|
154
|
+
name="name"
|
|
155
|
+
label="Full Name"
|
|
156
|
+
required
|
|
157
|
+
error={errors.name}
|
|
158
|
+
>
|
|
159
|
+
<input type="text" {...register('name')} />
|
|
160
|
+
</FormField>
|
|
161
|
+
|
|
162
|
+
<FormField
|
|
163
|
+
name="email"
|
|
164
|
+
label="Email Address"
|
|
165
|
+
required
|
|
166
|
+
hint="We'll never share your email"
|
|
167
|
+
error={errors.email}
|
|
168
|
+
>
|
|
169
|
+
<input type="email" {...register('email')} />
|
|
170
|
+
</FormField>
|
|
171
|
+
|
|
172
|
+
<Button type="submit">Submit</Button>
|
|
173
|
+
</form>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Fieldset for Related Inputs
|
|
179
|
+
```tsx
|
|
180
|
+
<fieldset>
|
|
181
|
+
<legend>Shipping Address</legend>
|
|
182
|
+
|
|
183
|
+
<FormField name="street" label="Street">
|
|
184
|
+
<input type="text" {...register('street')} />
|
|
185
|
+
</FormField>
|
|
186
|
+
|
|
187
|
+
<FormField name="city" label="City">
|
|
188
|
+
<input type="text" {...register('city')} />
|
|
189
|
+
</FormField>
|
|
190
|
+
</fieldset>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Radio Group
|
|
194
|
+
```tsx
|
|
195
|
+
import { Radio } from '@zoyth/simple-site-framework'
|
|
196
|
+
|
|
197
|
+
<Radio
|
|
198
|
+
name="plan"
|
|
199
|
+
label="Choose a plan"
|
|
200
|
+
options={[
|
|
201
|
+
{ value: 'basic', label: 'Basic' },
|
|
202
|
+
{ value: 'pro', label: 'Professional' },
|
|
203
|
+
{ value: 'enterprise', label: 'Enterprise' }
|
|
204
|
+
]}
|
|
205
|
+
value={selectedPlan}
|
|
206
|
+
onChange={setSelectedPlan}
|
|
207
|
+
/>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Loading States
|
|
211
|
+
|
|
212
|
+
### Loading Button
|
|
213
|
+
```tsx
|
|
214
|
+
<Button
|
|
215
|
+
onClick={handleSubmit}
|
|
216
|
+
loading={isSubmitting}
|
|
217
|
+
loadingText="Submitting..."
|
|
218
|
+
>
|
|
219
|
+
Submit Form
|
|
220
|
+
</Button>
|
|
221
|
+
```
|
|
222
|
+
**Screen reader**: "Submitting..., button, busy"
|
|
223
|
+
|
|
224
|
+
### Loading Content
|
|
225
|
+
```tsx
|
|
226
|
+
function DataTable() {
|
|
227
|
+
if (isLoading) {
|
|
228
|
+
return (
|
|
229
|
+
<div role="status" aria-live="polite">
|
|
230
|
+
<LoadingSpinner />
|
|
231
|
+
<span className="sr-only">Loading data...</span>
|
|
232
|
+
</div>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return <table>{/* data */}</table>
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Icon-Only Buttons
|
|
241
|
+
|
|
242
|
+
Always provide accessible labels:
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
// ❌ Bad - no label
|
|
246
|
+
<button onClick={handleClose}>
|
|
247
|
+
<Icons.X />
|
|
248
|
+
</button>
|
|
249
|
+
|
|
250
|
+
// ✅ Good - has aria-label
|
|
251
|
+
<Button
|
|
252
|
+
icon={<Icons.X />}
|
|
253
|
+
iconOnly
|
|
254
|
+
aria-label="Close dialog"
|
|
255
|
+
onClick={handleClose}
|
|
256
|
+
/>
|
|
257
|
+
|
|
258
|
+
// ✅ Also good - visual label hidden, but readable
|
|
259
|
+
<button onClick={handleClose}>
|
|
260
|
+
<Icons.X aria-hidden="true" />
|
|
261
|
+
<span className="sr-only">Close dialog</span>
|
|
262
|
+
</button>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Data Tables
|
|
266
|
+
|
|
267
|
+
### Basic Table
|
|
268
|
+
```tsx
|
|
269
|
+
<table>
|
|
270
|
+
<caption>Sales by Quarter</caption>
|
|
271
|
+
<thead>
|
|
272
|
+
<tr>
|
|
273
|
+
<th scope="col">Quarter</th>
|
|
274
|
+
<th scope="col">Sales</th>
|
|
275
|
+
<th scope="col">Growth</th>
|
|
276
|
+
</tr>
|
|
277
|
+
</thead>
|
|
278
|
+
<tbody>
|
|
279
|
+
<tr>
|
|
280
|
+
<th scope="row">Q1 2024</th>
|
|
281
|
+
<td>$125,000</td>
|
|
282
|
+
<td>+12%</td>
|
|
283
|
+
</tr>
|
|
284
|
+
</tbody>
|
|
285
|
+
</table>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Sortable Headers
|
|
289
|
+
```tsx
|
|
290
|
+
<th scope="col">
|
|
291
|
+
<button
|
|
292
|
+
onClick={() => sortBy('name')}
|
|
293
|
+
aria-sort={sortColumn === 'name' ? sortDirection : 'none'}
|
|
294
|
+
>
|
|
295
|
+
Name
|
|
296
|
+
{sortColumn === 'name' && (
|
|
297
|
+
<span aria-label={sortDirection === 'asc' ? 'sorted ascending' : 'sorted descending'}>
|
|
298
|
+
{sortDirection === 'asc' ? '↑' : '↓'}
|
|
299
|
+
</span>
|
|
300
|
+
)}
|
|
301
|
+
</button>
|
|
302
|
+
</th>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Accordions & Disclosure
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
import { FAQAccordion } from '@zoyth/simple-site-framework'
|
|
309
|
+
|
|
310
|
+
<FAQAccordion
|
|
311
|
+
items={[
|
|
312
|
+
{
|
|
313
|
+
question: 'What is your return policy?',
|
|
314
|
+
answer: 'We offer 30-day returns...'
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
question: 'Do you ship internationally?',
|
|
318
|
+
answer: 'Yes, we ship worldwide...'
|
|
319
|
+
}
|
|
320
|
+
]}
|
|
321
|
+
locale="en"
|
|
322
|
+
/>
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Screen reader navigation:**
|
|
326
|
+
- Tab through questions
|
|
327
|
+
- Enter/Space to expand/collapse
|
|
328
|
+
- Arrow keys to navigate
|
|
329
|
+
- Content announces when expanded
|
|
330
|
+
|
|
331
|
+
## Breadcrumbs
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
<nav aria-label="Breadcrumb">
|
|
335
|
+
<ol>
|
|
336
|
+
<li>
|
|
337
|
+
<a href="/">Home</a>
|
|
338
|
+
</li>
|
|
339
|
+
<li>
|
|
340
|
+
<a href="/products">Products</a>
|
|
341
|
+
</li>
|
|
342
|
+
<li aria-current="page">
|
|
343
|
+
Product Details
|
|
344
|
+
</li>
|
|
345
|
+
</ol>
|
|
346
|
+
</nav>
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Pagination
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
<nav aria-label="Pagination">
|
|
353
|
+
<Button
|
|
354
|
+
onClick={prevPage}
|
|
355
|
+
disabled={currentPage === 1}
|
|
356
|
+
aria-label="Previous page"
|
|
357
|
+
>
|
|
358
|
+
Previous
|
|
359
|
+
</Button>
|
|
360
|
+
|
|
361
|
+
<span aria-current="page">
|
|
362
|
+
Page {currentPage} of {totalPages}
|
|
363
|
+
</span>
|
|
364
|
+
|
|
365
|
+
<Button
|
|
366
|
+
onClick={nextPage}
|
|
367
|
+
disabled={currentPage === totalPages}
|
|
368
|
+
aria-label="Next page"
|
|
369
|
+
>
|
|
370
|
+
Next
|
|
371
|
+
</Button>
|
|
372
|
+
</nav>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Tabs
|
|
376
|
+
|
|
377
|
+
```tsx
|
|
378
|
+
import { Tabs } from '@zoyth/simple-site-framework'
|
|
379
|
+
|
|
380
|
+
<Tabs
|
|
381
|
+
tabs={[
|
|
382
|
+
{ value: 'tab1', label: 'Overview', content: <Overview /> },
|
|
383
|
+
{ value: 'tab2', label: 'Details', content: <Details /> },
|
|
384
|
+
{ value: 'tab3', label: 'Reviews', content: <Reviews /> }
|
|
385
|
+
]}
|
|
386
|
+
value={activeTab}
|
|
387
|
+
onValueChange={setActiveTab}
|
|
388
|
+
/>
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Screen reader navigation:**
|
|
392
|
+
- Tab to focus tab list
|
|
393
|
+
- Arrow keys to navigate tabs
|
|
394
|
+
- Enter to activate selected tab
|
|
395
|
+
|
|
396
|
+
## Tooltips
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
import { Button } from '@zoyth/simple-site-framework'
|
|
400
|
+
|
|
401
|
+
<Button
|
|
402
|
+
disabled={!isValid}
|
|
403
|
+
disabledTooltip="Please fill all required fields"
|
|
404
|
+
>
|
|
405
|
+
Submit
|
|
406
|
+
</Button>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**For non-disabled tooltips:**
|
|
410
|
+
```tsx
|
|
411
|
+
<button
|
|
412
|
+
aria-describedby="tooltip-1"
|
|
413
|
+
onMouseEnter={() => setShowTooltip(true)}
|
|
414
|
+
onMouseLeave={() => setShowTooltip(false)}
|
|
415
|
+
onFocus={() => setShowTooltip(true)}
|
|
416
|
+
onBlur={() => setShowTooltip(false)}
|
|
417
|
+
>
|
|
418
|
+
Help
|
|
419
|
+
</button>
|
|
420
|
+
{showTooltip && (
|
|
421
|
+
<div id="tooltip-1" role="tooltip">
|
|
422
|
+
Additional information
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Responsive Images
|
|
428
|
+
|
|
429
|
+
```tsx
|
|
430
|
+
<img
|
|
431
|
+
src="/images/product.jpg"
|
|
432
|
+
alt="Blue ceramic vase with geometric pattern"
|
|
433
|
+
loading="lazy"
|
|
434
|
+
/>
|
|
435
|
+
|
|
436
|
+
// For decorative images
|
|
437
|
+
<img
|
|
438
|
+
src="/images/decorative-pattern.svg"
|
|
439
|
+
alt=""
|
|
440
|
+
aria-hidden="true"
|
|
441
|
+
/>
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## Landmarks
|
|
445
|
+
|
|
446
|
+
Use semantic HTML5 elements:
|
|
447
|
+
|
|
448
|
+
```tsx
|
|
449
|
+
<>
|
|
450
|
+
<header>{/* Site header */}</header>
|
|
451
|
+
|
|
452
|
+
<nav aria-label="Main navigation">
|
|
453
|
+
{/* Primary navigation */}
|
|
454
|
+
</nav>
|
|
455
|
+
|
|
456
|
+
<main>
|
|
457
|
+
<section aria-labelledby="products-heading">
|
|
458
|
+
<h2 id="products-heading">Our Products</h2>
|
|
459
|
+
{/* Products */}
|
|
460
|
+
</section>
|
|
461
|
+
|
|
462
|
+
<aside aria-label="Related articles">
|
|
463
|
+
{/* Sidebar content */}
|
|
464
|
+
</aside>
|
|
465
|
+
</main>
|
|
466
|
+
|
|
467
|
+
<footer>{/* Site footer */}</footer>
|
|
468
|
+
</>
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
## Color Contrast
|
|
472
|
+
|
|
473
|
+
Ensure proper contrast ratios:
|
|
474
|
+
|
|
475
|
+
```tsx
|
|
476
|
+
// ❌ Bad - low contrast
|
|
477
|
+
<p style={{ color: '#999', background: '#fff' }}>
|
|
478
|
+
Light gray on white (3:1)
|
|
479
|
+
</p>
|
|
480
|
+
|
|
481
|
+
// ✅ Good - meets AA
|
|
482
|
+
<p style={{ color: '#666', background: '#fff' }}>
|
|
483
|
+
Dark gray on white (5.7:1)
|
|
484
|
+
</p>
|
|
485
|
+
|
|
486
|
+
// ✅ Better - use theme colors
|
|
487
|
+
<p className="text-gray-700 bg-white">
|
|
488
|
+
Framework colors meet AA standards
|
|
489
|
+
</p>
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Error Messages
|
|
493
|
+
|
|
494
|
+
### Inline Errors
|
|
495
|
+
```tsx
|
|
496
|
+
<FormField
|
|
497
|
+
name="email"
|
|
498
|
+
label="Email"
|
|
499
|
+
error={{ message: 'Please enter a valid email address' }}
|
|
500
|
+
>
|
|
501
|
+
<input
|
|
502
|
+
type="email"
|
|
503
|
+
aria-invalid={!!errors.email}
|
|
504
|
+
aria-describedby="email-error"
|
|
505
|
+
/>
|
|
506
|
+
</FormField>
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### Summary of Errors
|
|
510
|
+
```tsx
|
|
511
|
+
{Object.keys(errors).length > 0 && (
|
|
512
|
+
<div role="alert" aria-live="assertive">
|
|
513
|
+
<h2>Please fix the following errors:</h2>
|
|
514
|
+
<ul>
|
|
515
|
+
{Object.entries(errors).map(([field, error]) => (
|
|
516
|
+
<li key={field}>
|
|
517
|
+
<a href={`#${field}`}>{error.message}</a>
|
|
518
|
+
</li>
|
|
519
|
+
))}
|
|
520
|
+
</ul>
|
|
521
|
+
</div>
|
|
522
|
+
)}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Resources
|
|
526
|
+
|
|
527
|
+
- [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/patterns/)
|
|
528
|
+
- [Inclusive Components](https://inclusive-components.design/)
|
|
529
|
+
- [A11y Project Checklist](https://www.a11yproject.com/checklist/)
|