@zoyth/simple-site-framework 1.1.0 → 1.1.1

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.
@@ -0,0 +1,275 @@
1
+ # NewsletterSignup
2
+
3
+ Email capture form with multiple layout variants, size options, client-side validation, honeypot spam protection, and bilingual support (EN/FR).
4
+
5
+ ## Import
6
+
7
+ ```typescript
8
+ import { NewsletterSignup } from '@zoyth/simple-site-framework';
9
+ ```
10
+
11
+ **Type:** Client Component (`'use client'`)
12
+
13
+ ## Basic Usage
14
+
15
+ ```typescript
16
+ <NewsletterSignup
17
+ onSubmit={async (data) => {
18
+ const res = await fetch('/api/subscribe', {
19
+ method: 'POST',
20
+ body: JSON.stringify(data),
21
+ });
22
+ const json = await res.json();
23
+ return { success: json.ok };
24
+ }}
25
+ />
26
+ ```
27
+
28
+ ## Props
29
+
30
+ ### Required Props
31
+
32
+ | Prop | Type | Description |
33
+ |------|------|-------------|
34
+ | `onSubmit` | `(data: NewsletterSubmitData) => Promise<NewsletterResponse>` | Async handler called on form submission. Receives `{ email: string; name?: string }` and must return `{ success: boolean; message?: string; error?: string }` |
35
+
36
+ ### Optional Props
37
+
38
+ | Prop | Type | Default | Description |
39
+ |------|------|---------|-------------|
40
+ | `variant` | `'inline' \| 'stacked' \| 'minimal' \| 'card'` | `'stacked'` | Layout variant (see Variants below) |
41
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Controls text size and input/button padding |
42
+ | `showName` | `boolean` | `false` | Show a name input field (hidden in `minimal` variant) |
43
+ | `showPrivacy` | `boolean` | `false` | Show a privacy policy checkbox that must be accepted |
44
+ | `privacyUrl` | `string` | `'/privacy'` | URL the privacy policy link points to |
45
+ | `locale` | `string` | `'en'` | Current locale. Controls all default labels and validation messages |
46
+ | `buttonText` | `LocalizedString \| string` | `'Subscribe'` / `'S'abonner'` | Custom submit button text |
47
+ | `emailPlaceholder` | `LocalizedString \| string` | `'you@example.com'` / `'vous@exemple.com'` | Custom email placeholder |
48
+ | `namePlaceholder` | `LocalizedString \| string` | `'Your name'` / `'Votre nom'` | Custom name field placeholder |
49
+ | `successMessage` | `LocalizedString \| string` | `'Thank you for subscribing!'` / `'Merci pour votre inscription !'` | Custom success message |
50
+ | `className` | `string` | - | Additional CSS classes on the outer container |
51
+
52
+ ### Type Definitions
53
+
54
+ ```typescript
55
+ interface NewsletterSubmitData {
56
+ email: string;
57
+ name?: string;
58
+ }
59
+
60
+ interface NewsletterResponse {
61
+ success: boolean;
62
+ message?: string;
63
+ error?: string;
64
+ }
65
+ ```
66
+
67
+ ## Variants
68
+
69
+ ### Stacked (Default)
70
+
71
+ Fields and button arranged vertically with full-width inputs and labels:
72
+
73
+ ```typescript
74
+ <NewsletterSignup
75
+ onSubmit={handleSubscribe}
76
+ variant="stacked"
77
+ />
78
+ ```
79
+
80
+ **Use for:** Sidebar widgets, dedicated signup sections, anywhere with limited horizontal space.
81
+
82
+ ### Inline
83
+
84
+ Fields and button arranged horizontally in a single row:
85
+
86
+ ```typescript
87
+ <NewsletterSignup
88
+ onSubmit={handleSubscribe}
89
+ variant="inline"
90
+ />
91
+ ```
92
+
93
+ **Use for:** Footer bars, hero sections, compact horizontal layouts. Labels are replaced with `aria-label` attributes for accessibility.
94
+
95
+ ### Minimal
96
+
97
+ Email field and button only, no labels displayed:
98
+
99
+ ```typescript
100
+ <NewsletterSignup
101
+ onSubmit={handleSubscribe}
102
+ variant="minimal"
103
+ />
104
+ ```
105
+
106
+ The `showName` prop is ignored in this variant. Accessibility labels are provided via `aria-label`.
107
+
108
+ **Use for:** Compact spaces, inline CTAs, anywhere a lightweight form is needed.
109
+
110
+ ### Card
111
+
112
+ Wrapped in a bordered card with padding and subtle shadow:
113
+
114
+ ```typescript
115
+ <NewsletterSignup
116
+ onSubmit={handleSubscribe}
117
+ variant="card"
118
+ />
119
+ ```
120
+
121
+ **Use for:** Standalone signup blocks, sidebar cards, visually distinct callouts.
122
+
123
+ ## Sizes
124
+
125
+ | Size | Text | Input Padding | Button Padding |
126
+ |------|------|---------------|----------------|
127
+ | `sm` | `text-sm` | `px-3 py-1.5` | `px-4 py-1.5` |
128
+ | `md` | `text-base` | `px-4 py-2` | `px-6 py-2` |
129
+ | `lg` | `text-lg` | `px-5 py-3` | `px-8 py-3` |
130
+
131
+ ```typescript
132
+ <NewsletterSignup onSubmit={handleSubscribe} size="lg" />
133
+ ```
134
+
135
+ ## Localization
136
+
137
+ All default labels adapt based on the `locale` prop:
138
+
139
+ | Label | English (`'en'`) | French (`'fr'`) |
140
+ |-------|-------------------|------------------|
141
+ | Subscribe button | "Subscribe" | "S'abonner" |
142
+ | Email label | "Email address" | "Adresse courriel" |
143
+ | Name label | "Name" | "Nom" |
144
+ | Email placeholder | "you@example.com" | "vous@exemple.com" |
145
+ | Name placeholder | "Your name" | "Votre nom" |
146
+ | Email required error | "Please enter your email address." | "Veuillez entrer votre adresse courriel." |
147
+ | Email invalid error | "Please enter a valid email address." | "Veuillez entrer une adresse courriel valide." |
148
+ | Privacy required error | "You must accept the privacy policy." | "Vous devez accepter la politique de confidentialite." |
149
+ | Privacy text | "I agree to the" | "J'accepte la" |
150
+ | Privacy link | "privacy policy" | "politique de confidentialite" |
151
+ | Success message | "Thank you for subscribing!" | "Merci pour votre inscription !" |
152
+ | Generic error | "Something went wrong. Please try again." | "Une erreur est survenue. Veuillez reessayer." |
153
+ | Submitting state | "Subscribing..." | "Inscription..." |
154
+
155
+ ## Examples
156
+
157
+ ### With Name Field
158
+
159
+ ```typescript
160
+ <NewsletterSignup
161
+ onSubmit={handleSubscribe}
162
+ showName={true}
163
+ />
164
+ ```
165
+
166
+ ### With Privacy Checkbox
167
+
168
+ ```typescript
169
+ <NewsletterSignup
170
+ onSubmit={handleSubscribe}
171
+ showPrivacy={true}
172
+ privacyUrl="/en/privacy-policy"
173
+ />
174
+ ```
175
+
176
+ The form will not submit until the checkbox is checked.
177
+
178
+ ### French Locale
179
+
180
+ ```typescript
181
+ <NewsletterSignup
182
+ onSubmit={handleSubscribe}
183
+ locale="fr"
184
+ variant="card"
185
+ />
186
+ ```
187
+
188
+ ### Custom Text
189
+
190
+ ```typescript
191
+ <NewsletterSignup
192
+ onSubmit={handleSubscribe}
193
+ buttonText={{ en: 'Join the list', fr: 'Rejoindre la liste' }}
194
+ emailPlaceholder="enter@your.email"
195
+ successMessage={{ en: 'You are in!', fr: 'Vous etes inscrit !' }}
196
+ />
197
+ ```
198
+
199
+ ### Inline in Footer
200
+
201
+ ```typescript
202
+ <footer className="bg-gray-900 text-white py-12">
203
+ <div className="max-w-4xl mx-auto">
204
+ <h3 className="text-lg font-semibold mb-4">Stay updated</h3>
205
+ <NewsletterSignup
206
+ onSubmit={handleSubscribe}
207
+ variant="inline"
208
+ size="sm"
209
+ />
210
+ </div>
211
+ </footer>
212
+ ```
213
+
214
+ ### Card with All Options
215
+
216
+ ```typescript
217
+ <NewsletterSignup
218
+ onSubmit={handleSubscribe}
219
+ variant="card"
220
+ size="lg"
221
+ showName={true}
222
+ showPrivacy={true}
223
+ privacyUrl="/privacy"
224
+ locale="en"
225
+ buttonText="Get Updates"
226
+ emailPlaceholder="your@email.com"
227
+ namePlaceholder="First name"
228
+ successMessage="Welcome aboard!"
229
+ />
230
+ ```
231
+
232
+ ## Validation
233
+
234
+ The component performs client-side validation before calling `onSubmit`:
235
+
236
+ 1. **Empty email** - shows "Please enter your email address."
237
+ 2. **Invalid email format** - uses the regex `/^[^\s@]+@[^\s@]+\.[^\s@]+$/` and shows "Please enter a valid email address."
238
+ 3. **Privacy checkbox** - if `showPrivacy` is `true` and unchecked, shows "You must accept the privacy policy."
239
+
240
+ Validation messages are localized based on the `locale` prop.
241
+
242
+ ## Spam Protection
243
+
244
+ The component includes a hidden honeypot field. If a bot fills it in, the form silently does nothing on submit (no error, no API call). The honeypot field is:
245
+
246
+ - Hidden via CSS (`className="hidden"`)
247
+ - Marked with `aria-hidden="true"`
248
+ - Uses `tabIndex={-1}` to prevent keyboard focus
249
+ - Has `autoComplete="off"` to prevent browser autofill
250
+
251
+ ## Form States
252
+
253
+ The component manages four internal states:
254
+
255
+ | State | Behavior |
256
+ |-------|----------|
257
+ | `idle` | Default state. Form is interactive |
258
+ | `submitting` | Inputs and button are disabled. Button text changes to "Subscribing..." |
259
+ | `success` | Form is replaced with the success message (rendered as a `role="status"` paragraph) |
260
+ | `error` | Error message appears below the form as a `role="alert"` paragraph |
261
+
262
+ ## Accessibility
263
+
264
+ - Email input has `required` attribute
265
+ - Labels are rendered for `stacked` and `card` variants
266
+ - `aria-label` attributes are used for `inline` and `minimal` variants where visual labels are omitted
267
+ - Error messages use `role="alert"` for screen reader announcement
268
+ - Success message uses `role="status"`
269
+ - All inputs respect `disabled` state during submission
270
+
271
+ ## See Also
272
+
273
+ - **[Button](./ui/Button.md)** - Button component used throughout the framework
274
+ - **[ContactSection](./sections/ContactSection.md)** - Full contact form section (for more complex form needs)
275
+ - **[BlogIndex](./BlogIndex.md)** - Blog listing page where a newsletter signup might be placed
@@ -0,0 +1,146 @@
1
+ # SkipLink
2
+
3
+ Accessible skip navigation link for keyboard users. Hidden by default, it appears when focused via Tab and allows users to bypass repetitive navigation elements and jump directly to the main content area.
4
+
5
+ ## Import
6
+
7
+ ```typescript
8
+ import { SkipLink } from '@zoyth/simple-site-framework';
9
+ ```
10
+
11
+ **Type:** Client Component (`'use client'`)
12
+
13
+ **Source:** `src/components/a11y/SkipLink.tsx`
14
+
15
+ ## Basic Usage
16
+
17
+ ```typescript
18
+ // In your root layout, before the header
19
+ <SkipLink href="#main-content">Skip to main content</SkipLink>
20
+ <Header />
21
+ <main id="main-content" tabIndex={-1}>
22
+ {children}
23
+ </main>
24
+ ```
25
+
26
+ ## Props
27
+
28
+ ### Required Props
29
+
30
+ | Prop | Type | Description |
31
+ |------|------|-------------|
32
+ | `href` | `string` | CSS selector for the target element to skip to (e.g., `"#main-content"`) |
33
+ | `children` | `ReactNode` | Link text displayed when the component is focused |
34
+
35
+ ### Optional Props
36
+
37
+ | Prop | Type | Default | Description |
38
+ |------|------|---------|-------------|
39
+ | `className` | `string` | - | Additional CSS classes merged with the default styles |
40
+
41
+ ## How It Works
42
+
43
+ 1. The link is positioned absolutely at the top-left of the page and translated off-screen (`-translate-y-full`).
44
+ 2. When a keyboard user presses Tab, the link receives focus and transitions into view (`focus:translate-y-0`).
45
+ 3. On click, the component prevents the default anchor behavior and instead:
46
+ - Finds the target element using `document.querySelector(href)`
47
+ - Calls `.focus()` on the target element
48
+ - Scrolls the target into view with `{ behavior: 'smooth', block: 'start' }`
49
+
50
+ This approach works even when the target element is not natively focusable, as long as it has `tabIndex={-1}`.
51
+
52
+ ## Examples
53
+
54
+ ### Standard Layout Integration
55
+
56
+ ```typescript
57
+ export default function RootLayout({ children }) {
58
+ return (
59
+ <html>
60
+ <body>
61
+ <SkipLink href="#main-content">Skip to main content</SkipLink>
62
+ <Header />
63
+ <main id="main-content" tabIndex={-1}>
64
+ {children}
65
+ </main>
66
+ <Footer />
67
+ </body>
68
+ </html>
69
+ );
70
+ }
71
+ ```
72
+
73
+ ### French Label
74
+
75
+ ```typescript
76
+ <SkipLink href="#contenu-principal">
77
+ Aller au contenu principal
78
+ </SkipLink>
79
+ ```
80
+
81
+ ### Multiple Skip Links
82
+
83
+ For pages with complex navigation, you can provide multiple skip links:
84
+
85
+ ```typescript
86
+ <div>
87
+ <SkipLink href="#main-content">Skip to main content</SkipLink>
88
+ <SkipLink href="#sidebar">Skip to sidebar</SkipLink>
89
+ </div>
90
+
91
+ {/* Later in the page */}
92
+ <main id="main-content" tabIndex={-1}>...</main>
93
+ <aside id="sidebar" tabIndex={-1}>...</aside>
94
+ ```
95
+
96
+ ### Custom Styling
97
+
98
+ ```typescript
99
+ <SkipLink
100
+ href="#main-content"
101
+ className="text-lg rounded-br-lg"
102
+ >
103
+ Skip to main content
104
+ </SkipLink>
105
+ ```
106
+
107
+ ## Default Styles
108
+
109
+ The component applies these styles by default:
110
+
111
+ | State | Styles |
112
+ |-------|--------|
113
+ | Hidden (default) | `absolute left-0 top-0 z-50 -translate-y-full` |
114
+ | Visible (focused) | `focus:translate-y-0` with `transition-transform` |
115
+ | Visual | `bg-primary text-white px-4 py-2 font-medium` |
116
+ | Focus ring | `focus:ring-2 focus:ring-offset-2 focus:ring-primary` with `focus:outline-none` |
117
+
118
+ Custom classes passed via `className` are merged using the `cn()` utility, so they can override any default.
119
+
120
+ ## Target Element Setup
121
+
122
+ The target element must:
123
+
124
+ 1. Have an `id` that matches the `href` value (without the `#`)
125
+ 2. Have `tabIndex={-1}` so it can receive programmatic focus
126
+
127
+ ```typescript
128
+ {/* Correct */}
129
+ <main id="main-content" tabIndex={-1}>
130
+
131
+ {/* The href must include the # */}
132
+ <SkipLink href="#main-content">Skip to main content</SkipLink>
133
+ ```
134
+
135
+ ## Accessibility
136
+
137
+ - Meets WCAG 2.1 Success Criterion 2.4.1 (Bypass Blocks)
138
+ - Only visible to keyboard users (hidden from mouse/touch users)
139
+ - Uses semantic `<a>` element
140
+ - Focus ring provides clear visual indication
141
+ - Smooth scroll provides orientation context after the skip
142
+
143
+ ## See Also
144
+
145
+ - **[Header](./layout/Header.md)** - Site header that the skip link typically bypasses
146
+ - **[PageLayout](./layout/PageLayout.md)** - Standard page wrapper where skip links are commonly placed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoyth/simple-site-framework",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "A configuration-driven framework for building professional service websites",
5
5
  "keywords": [
6
6
  "framework",