@kolkrabbi/kol-component 0.89.0 → 0.90.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.
- package/package.json +1 -1
- package/src/index.js +1 -0
- package/src/organisms/NewsletterBand.jsx +8 -118
- package/src/organisms/SectionNewsletter.jsx +160 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.90.1",
|
|
4
4
|
"description": "KOL design-system components — atoms through organisms, emitting canonical kol-* classes. Pairs with @kolkrabbi/kol-theme for styling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/index.js
CHANGED
|
@@ -135,6 +135,7 @@ export { default as MediaLibrary, MediaLibraryProvider, useMediaLibrary, MediaPi
|
|
|
135
135
|
export { default as MediaTileGallery } from './organisms/MediaTileGallery.jsx'
|
|
136
136
|
export { default as MediaViewer } from './organisms/MediaViewer.jsx'
|
|
137
137
|
export { default as SettingsPanel, SettingsRow, SettingsSwitch, SettingsChoice, SettingsChipRow, SettingsFooter } from './organisms/SettingsPanel.jsx'
|
|
138
|
+
export { default as SectionNewsletter } from './organisms/SectionNewsletter.jsx'
|
|
138
139
|
export { default as NewsletterBand } from './organisms/NewsletterBand.jsx'
|
|
139
140
|
export { default as RecordManager } from './organisms/RecordManager.jsx'
|
|
140
141
|
export { default as SpectrumGrid } from './organisms/SpectrumGrid.jsx'
|
|
@@ -1,122 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import Input from '../atoms/Input.jsx'
|
|
3
|
-
import Button from '../atoms/Button.jsx'
|
|
1
|
+
import SectionNewsletter from './SectionNewsletter.jsx'
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* success / error, driven by the promise returned from `onSubmit(email)`.
|
|
12
|
-
* No fetch lives here: the consumer's handler posts wherever it wants;
|
|
13
|
-
* resolve → success (field clears), reject → error (logged). An empty submit
|
|
14
|
-
* errors immediately without calling the handler, and the submit Button
|
|
15
|
-
* disables while the promise is pending. Native `type="email"` validation
|
|
16
|
-
* still gates malformed addresses before the handler runs.
|
|
17
|
-
*
|
|
18
|
-
* A11y: the Input carries aria-required and, on error, aria-describedby
|
|
19
|
-
* pointing at the error line; the success line is role=status
|
|
20
|
-
* aria-live=polite, the error line role=alert aria-live=assertive. Field and
|
|
21
|
-
* error ids are useId-generated (or `inputId`) so multiple bands mount
|
|
22
|
-
* without collisions.
|
|
23
|
-
*
|
|
24
|
-
* @param {ReactNode} title heading (kol-sans-display-01)
|
|
25
|
-
* @param {ReactNode} description lede under the heading (kol-mono-14)
|
|
26
|
-
* @param {string} placeholder email Input placeholder
|
|
27
|
-
* @param {ReactNode} submitLabel submit Button label (also its accessible name)
|
|
28
|
-
* @param {Function} onSubmit (email) => Promise|void — resolve = success, reject = error
|
|
29
|
-
* @param {ReactNode} successCopy success status line
|
|
30
|
-
* @param {ReactNode} errorCopy error status line
|
|
31
|
-
* @param {string} id anchor id on the section (e.g. "signup")
|
|
32
|
-
* @param {string} inputId id override for the email input (default useId-generated)
|
|
33
|
-
* @param {string} className extra classes on the section
|
|
4
|
+
* @deprecated 2026-08-27 — `NewsletterBand` is `SectionNewsletter` under its
|
|
5
|
+
* old name: the newsletter joined the section family (SectionNewsletter,
|
|
6
|
+
* kol-website). `title` → `headline`, `description` → `body`; everything else
|
|
7
|
+
* passes through. On the retirement ledger (04-retirements.md) — dropped when
|
|
8
|
+
* no repo imports it.
|
|
34
9
|
*/
|
|
35
|
-
export default function NewsletterBand({
|
|
36
|
-
title
|
|
37
|
-
description,
|
|
38
|
-
placeholder,
|
|
39
|
-
submitLabel,
|
|
40
|
-
onSubmit,
|
|
41
|
-
successCopy = 'Thanks for subscribing!',
|
|
42
|
-
errorCopy = 'Please enter a valid email address.',
|
|
43
|
-
id,
|
|
44
|
-
inputId,
|
|
45
|
-
className = '',
|
|
46
|
-
}) {
|
|
47
|
-
const [email, setEmail] = useState('')
|
|
48
|
-
const [status, setStatus] = useState('idle') // 'idle' | 'submitting' | 'success' | 'error'
|
|
49
|
-
|
|
50
|
-
const autoId = useId()
|
|
51
|
-
const emailId = inputId ?? `${autoId}email`
|
|
52
|
-
const errorId = `${emailId}-error`
|
|
53
|
-
|
|
54
|
-
const handleSubmit = async (e) => {
|
|
55
|
-
e.preventDefault()
|
|
56
|
-
|
|
57
|
-
if (!email) {
|
|
58
|
-
setStatus('error')
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
if (!onSubmit) return
|
|
62
|
-
|
|
63
|
-
setStatus('submitting')
|
|
64
|
-
try {
|
|
65
|
-
await onSubmit(email)
|
|
66
|
-
setStatus('success')
|
|
67
|
-
setEmail('')
|
|
68
|
-
} catch (error) {
|
|
69
|
-
console.error('Newsletter signup error:', error)
|
|
70
|
-
setStatus('error')
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
<section
|
|
76
|
-
id={id}
|
|
77
|
-
className={`w-full flex flex-col items-center justify-center text-center ${className}`.trim()}
|
|
78
|
-
>
|
|
79
|
-
<div className="max-w-[1400px] mx-auto py-24">
|
|
80
|
-
<h2 className="kol-sans-display-01 mb-6">{title}</h2>
|
|
81
|
-
|
|
82
|
-
<p className="kol-mono-14 text-auto mb-12 mx-auto max-w-[64rem]">{description}</p>
|
|
83
|
-
|
|
84
|
-
<form
|
|
85
|
-
onSubmit={handleSubmit}
|
|
86
|
-
className="flex flex-col gap-4 mb-16 sm:flex-row sm:items-center sm:justify-center sm:gap-3"
|
|
87
|
-
>
|
|
88
|
-
<Input
|
|
89
|
-
id={emailId}
|
|
90
|
-
type="email"
|
|
91
|
-
placeholder={placeholder}
|
|
92
|
-
value={email}
|
|
93
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
94
|
-
size="md"
|
|
95
|
-
aria-required="true"
|
|
96
|
-
aria-describedby={status === 'error' ? errorId : undefined}
|
|
97
|
-
className="w-full sm:max-w-[400px] md:max-w-[520px]"
|
|
98
|
-
/>
|
|
99
|
-
<Button
|
|
100
|
-
type="submit"
|
|
101
|
-
variant="primary"
|
|
102
|
-
disabled={status === 'submitting'}
|
|
103
|
-
className="w-full sm:w-auto"
|
|
104
|
-
>
|
|
105
|
-
{submitLabel}
|
|
106
|
-
</Button>
|
|
107
|
-
</form>
|
|
108
|
-
|
|
109
|
-
{status === 'success' && (
|
|
110
|
-
<p className="kol-mono-14 text-auto opacity-80" role="status" aria-live="polite">
|
|
111
|
-
{successCopy}
|
|
112
|
-
</p>
|
|
113
|
-
)}
|
|
114
|
-
{status === 'error' && (
|
|
115
|
-
<p id={errorId} className="kol-mono-14 text-auto opacity-80" role="alert" aria-live="assertive">
|
|
116
|
-
{errorCopy}
|
|
117
|
-
</p>
|
|
118
|
-
)}
|
|
119
|
-
</div>
|
|
120
|
-
</section>
|
|
121
|
-
)
|
|
10
|
+
export default function NewsletterBand({ title, description, ...rest }) {
|
|
11
|
+
return <SectionNewsletter headline={title} body={description} {...rest} />
|
|
122
12
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { useId, useState } from 'react'
|
|
2
|
+
import Input from '../atoms/Input.jsx'
|
|
3
|
+
import Button from '../atoms/Button.jsx'
|
|
4
|
+
import SectionText from '../molecules/SectionText.jsx'
|
|
5
|
+
import useSectionTheme from '../hooks/useSectionTheme.js'
|
|
6
|
+
import { minHeightClass } from './sectionHeights.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SectionNewsletter — the newsletter card of the section family
|
|
10
|
+
* (SectionNewsletter, kol-website 2026-08-27 — user: the newsletter as a card
|
|
11
|
+
* in the family, same ladder, same cap, content centred on y, not its own
|
|
12
|
+
* thing). `SectionText` (label · headline · body, centred) with the email
|
|
13
|
+
* form in its children slot: an email Input + submit Button and an inline
|
|
14
|
+
* success / error status line. `NewsletterBand` is the deprecated alias
|
|
15
|
+
* (`title` → `headline`, `description` → `body`).
|
|
16
|
+
*
|
|
17
|
+
* Owns only the local form state — the email value and idle / submitting /
|
|
18
|
+
* success / error, driven by the promise returned from `onSubmit(email)`.
|
|
19
|
+
* No fetch lives here: the consumer's handler posts wherever it wants;
|
|
20
|
+
* resolve → success (field clears), reject → error (logged). An empty submit
|
|
21
|
+
* errors immediately without calling the handler, and the submit Button
|
|
22
|
+
* disables while the promise is pending. Native `type="email"` validation
|
|
23
|
+
* still gates malformed addresses before the handler runs.
|
|
24
|
+
*
|
|
25
|
+
* A11y: the Input carries aria-required and, on error, aria-describedby
|
|
26
|
+
* pointing at the error line; the success line is role=status
|
|
27
|
+
* aria-live=polite, the error line role=alert aria-live=assertive. Field and
|
|
28
|
+
* error ids are useId-generated (or `inputId`) so multiple sections mount
|
|
29
|
+
* without collisions.
|
|
30
|
+
*
|
|
31
|
+
* @param {'full'|'80'|'60'|'40'|string} [height='60'] min-height on the family's ladder — full = 100dvh,
|
|
32
|
+
* 80 = 70svh / 80vh, 60 = 50svh / 60vh (default), 40 = 35svh / 40vh; content stays vertically centred inside it
|
|
33
|
+
* @param {'inverse'|'light'|'dark'} theme the paired theme of whatever is live, or a pinned one — stamped on the section
|
|
34
|
+
* @param {ReactNode} label eyebrow above the headline (uppercase by role)
|
|
35
|
+
* @param {ReactNode} headline heading (display-01 by default; `headlineSize` picks another role)
|
|
36
|
+
* @param {string} headlineSize SectionText role (default 'display-01')
|
|
37
|
+
* @param {ReactNode} body lede under the heading
|
|
38
|
+
* @param {string} placeholder email Input placeholder
|
|
39
|
+
* @param {ReactNode} submitLabel submit Button label (also its accessible name)
|
|
40
|
+
* @param {Function} onSubmit (email) => Promise|void — resolve = success, reject = error
|
|
41
|
+
* @param {ReactNode} successCopy success status line
|
|
42
|
+
* @param {ReactNode} errorCopy error status line
|
|
43
|
+
* @param {string} id anchor id on the section (e.g. "signup")
|
|
44
|
+
* @param {string} inputId id override for the email input (default useId-generated)
|
|
45
|
+
* @param {object} slotClass · slotStyle per-slot class / style on the SectionText (reveal seam)
|
|
46
|
+
* @param {string} className extra classes on the section
|
|
47
|
+
*/
|
|
48
|
+
export default function SectionNewsletter({
|
|
49
|
+
height = '60',
|
|
50
|
+
theme,
|
|
51
|
+
label,
|
|
52
|
+
headline,
|
|
53
|
+
headlineSize = 'display-01',
|
|
54
|
+
body,
|
|
55
|
+
placeholder,
|
|
56
|
+
submitLabel,
|
|
57
|
+
onSubmit,
|
|
58
|
+
successCopy = 'Thanks for subscribing!',
|
|
59
|
+
errorCopy = 'Please enter a valid email address.',
|
|
60
|
+
id,
|
|
61
|
+
inputId,
|
|
62
|
+
slotClass,
|
|
63
|
+
slotStyle,
|
|
64
|
+
className = '',
|
|
65
|
+
}) {
|
|
66
|
+
const [email, setEmail] = useState('')
|
|
67
|
+
const [status, setStatus] = useState('idle') // 'idle' | 'submitting' | 'success' | 'error'
|
|
68
|
+
const [themeRef, themeStamp] = useSectionTheme(theme)
|
|
69
|
+
|
|
70
|
+
const autoId = useId()
|
|
71
|
+
const emailId = inputId ?? `${autoId}email`
|
|
72
|
+
const errorId = `${emailId}-error`
|
|
73
|
+
|
|
74
|
+
const handleSubmit = async (e) => {
|
|
75
|
+
e.preventDefault()
|
|
76
|
+
|
|
77
|
+
if (!email) {
|
|
78
|
+
setStatus('error')
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if (!onSubmit) return
|
|
82
|
+
|
|
83
|
+
setStatus('submitting')
|
|
84
|
+
try {
|
|
85
|
+
await onSubmit(email)
|
|
86
|
+
setStatus('success')
|
|
87
|
+
setEmail('')
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Newsletter signup error:', error)
|
|
90
|
+
setStatus('error')
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<section
|
|
96
|
+
id={id}
|
|
97
|
+
ref={themeRef}
|
|
98
|
+
data-theme={themeStamp}
|
|
99
|
+
className={`kol-section-newsletter w-full flex flex-col justify-center py-24 ${theme ? 'bg-surface-primary text-auto' : ''} ${minHeightClass(height)} ${className}`.replace(/\s+/g, ' ').trim()}
|
|
100
|
+
>
|
|
101
|
+
{/* the family's ONE cap — the shell's --kol-container-max ladder — and
|
|
102
|
+
* inside it the lede's MEASURE on a wrapper (SectionNewsletterForm,
|
|
103
|
+
* 2026-08-27): a measure is a layout seam, not a `bodyClass` override
|
|
104
|
+
* on SectionText — the organism renders the molecule bare */}
|
|
105
|
+
<div className="w-full max-w-[var(--kol-container-max,var(--kol-content-shell,1800px))] mx-auto text-center">
|
|
106
|
+
<div className="mx-auto max-w-[64rem]">
|
|
107
|
+
<SectionText
|
|
108
|
+
align="center"
|
|
109
|
+
label={label}
|
|
110
|
+
headline={headline}
|
|
111
|
+
headlineSize={headlineSize}
|
|
112
|
+
body={body}
|
|
113
|
+
gap="gap-6"
|
|
114
|
+
slotClass={slotClass}
|
|
115
|
+
slotStyle={slotStyle}
|
|
116
|
+
>
|
|
117
|
+
{/* `w-full`: SectionText centres its children as flex items, so
|
|
118
|
+
* without it the form shrink-wraps and the Input's `w-full` has
|
|
119
|
+
* nothing to fill — it rendered at its intrinsic ~190px */}
|
|
120
|
+
<form
|
|
121
|
+
onSubmit={handleSubmit}
|
|
122
|
+
className="flex w-full flex-col gap-4 pt-6 sm:flex-row sm:items-center sm:justify-center sm:gap-3"
|
|
123
|
+
>
|
|
124
|
+
<Input
|
|
125
|
+
id={emailId}
|
|
126
|
+
type="email"
|
|
127
|
+
placeholder={placeholder}
|
|
128
|
+
value={email}
|
|
129
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
130
|
+
size="md"
|
|
131
|
+
aria-required="true"
|
|
132
|
+
aria-describedby={status === 'error' ? errorId : undefined}
|
|
133
|
+
className="w-full sm:max-w-[400px] md:max-w-[520px]"
|
|
134
|
+
/>
|
|
135
|
+
<Button
|
|
136
|
+
type="submit"
|
|
137
|
+
variant="primary"
|
|
138
|
+
disabled={status === 'submitting'}
|
|
139
|
+
className="w-full sm:w-auto"
|
|
140
|
+
>
|
|
141
|
+
{submitLabel}
|
|
142
|
+
</Button>
|
|
143
|
+
</form>
|
|
144
|
+
|
|
145
|
+
{status === 'success' && (
|
|
146
|
+
<p className="kol-mono-14 text-auto opacity-80 pt-4" role="status" aria-live="polite">
|
|
147
|
+
{successCopy}
|
|
148
|
+
</p>
|
|
149
|
+
)}
|
|
150
|
+
{status === 'error' && (
|
|
151
|
+
<p id={errorId} className="kol-mono-14 text-auto opacity-80 pt-4" role="alert" aria-live="assertive">
|
|
152
|
+
{errorCopy}
|
|
153
|
+
</p>
|
|
154
|
+
)}
|
|
155
|
+
</SectionText>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</section>
|
|
159
|
+
)
|
|
160
|
+
}
|