@kolkrabbi/kol-component 0.75.0 → 0.76.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.0",
|
|
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",
|
|
@@ -81,6 +81,10 @@ function SlideMedia({ media, active = true, onEnded, onTimeUpdate }) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
+
* @deprecated 2026-08-26 as a consumer import — `SectionHero media={[…]}` is
|
|
85
|
+
* the one hero (SectionHeroRound2); this stays as its engine and renders
|
|
86
|
+
* unchanged. Removed from the barrel at the next major.
|
|
87
|
+
*
|
|
84
88
|
* FeaturedCarousel — a full-width carousel of featured media: each wide slide
|
|
85
89
|
* is a fixed-height frame with an image or HLS-video background and a centered
|
|
86
90
|
* OverlayGlassPanel (title / description / CTA), plus prev/next and optional
|
|
@@ -1,65 +1,38 @@
|
|
|
1
1
|
import Button from '../atoms/Button.jsx'
|
|
2
|
+
import SectionCta from './SectionCta.jsx'
|
|
2
3
|
|
|
3
4
|
/* taxonomy-ok: nests component's Button */
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* same family; this is the quiet mid-page tier (foundry pages, prints grid).
|
|
7
|
+
* @deprecated 2026-08-26 — `FoundryCTA` is `SectionCta variant="centered"`
|
|
8
|
+
* (SectionHeroRound2: a second way to render SectionText). Same render; the
|
|
9
|
+
* `action` object(s) become Buttons. Alias kept; removed at the next major.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* same `(href, event)` seam contract as WorkListItem — or omit it for plain
|
|
15
|
-
* anchors (external links, mailto:).
|
|
16
|
-
*
|
|
17
|
-
* @param {ReactNode} heading band heading (kol-sans-heading-02)
|
|
18
|
-
* @param {ReactNode} description supporting line (kol-mono-14, dimmed)
|
|
19
|
-
* @param {Object|Array} action one {href, label, variant?, target?, rel?} or an array;
|
|
20
|
-
* variant 'secondary' renders Button's outline treatment,
|
|
21
|
-
* anything else the primary fill
|
|
22
|
-
* @param {Function} onNavigate (href, event) => void — SPA-nav seam for internal links
|
|
23
|
-
* @param {string} className extra classes on the section
|
|
11
|
+
* @param {ReactNode} heading · description
|
|
12
|
+
* @param {Object|Array} action one {href, label, variant?, target?, rel?} or an array
|
|
13
|
+
* @param {Function} onNavigate (href, event) => void
|
|
24
14
|
*/
|
|
25
|
-
export default function FoundryCTA({
|
|
26
|
-
heading,
|
|
27
|
-
description,
|
|
28
|
-
action,
|
|
29
|
-
onNavigate,
|
|
30
|
-
className = '',
|
|
31
|
-
}) {
|
|
15
|
+
export default function FoundryCTA({ heading, description, action, onNavigate, className = '' }) {
|
|
32
16
|
const actions = (Array.isArray(action) ? action : [action]).filter(Boolean)
|
|
33
|
-
|
|
34
17
|
return (
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
rel={act.rel}
|
|
55
|
-
onClick={onNavigate ? (e) => onNavigate(act.href, e) : undefined}
|
|
56
|
-
>
|
|
57
|
-
{act.label}
|
|
58
|
-
</Button>
|
|
59
|
-
))}
|
|
60
|
-
</div>
|
|
61
|
-
)}
|
|
62
|
-
</div>
|
|
63
|
-
</section>
|
|
18
|
+
<SectionCta
|
|
19
|
+
variant="centered"
|
|
20
|
+
headline={heading}
|
|
21
|
+
body={description}
|
|
22
|
+
className={className}
|
|
23
|
+
actions={actions.length > 0 ? actions.map((act, i) => (
|
|
24
|
+
<Button
|
|
25
|
+
key={i}
|
|
26
|
+
variant={act.variant === 'secondary' ? 'outline' : 'primary'}
|
|
27
|
+
size="md"
|
|
28
|
+
href={act.href}
|
|
29
|
+
target={act.target}
|
|
30
|
+
rel={act.rel}
|
|
31
|
+
onClick={onNavigate ? (e) => onNavigate(act.href, e) : undefined}
|
|
32
|
+
>
|
|
33
|
+
{act.label}
|
|
34
|
+
</Button>
|
|
35
|
+
)) : undefined}
|
|
36
|
+
/>
|
|
64
37
|
)
|
|
65
38
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Button from '../atoms/Button.jsx'
|
|
1
2
|
import SectionText from '../molecules/SectionText.jsx'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -8,6 +9,13 @@ import SectionText from '../molecules/SectionText.jsx'
|
|
|
8
9
|
* `CtaGlobal` is this component under its old name (alias kept). Columns sit
|
|
9
10
|
* side by side from `md` up and stack below. Row labels are uppercase by role.
|
|
10
11
|
*
|
|
12
|
+
* TWO VARIANTS. `editorial` (default): the two-column band above. `centered`
|
|
13
|
+
* (SectionHeroRound2, 2026-08-26): the quiet mid-page tier — a short rule, a
|
|
14
|
+
* heading, a mono line and Buttons, centred — which was `FoundryCTA`, a second
|
|
15
|
+
* way to render SectionText. FoundryCTA is its deprecated alias.
|
|
16
|
+
*
|
|
17
|
+
* @param {'editorial'|'centered'} variant
|
|
18
|
+
* @param {ReactNode} headline · body · actions centered: the SectionText slots
|
|
11
19
|
* @param {ReactNode} eyebrow left-column display wordmark (kol-sans-display-01)
|
|
12
20
|
* @param {ReactNode} promptLabel prompt-row label
|
|
13
21
|
* @param {ReactNode} heading prompt-row value
|
|
@@ -17,6 +25,10 @@ import SectionText from '../molecules/SectionText.jsx'
|
|
|
17
25
|
* @param {string} className extra classes on the section
|
|
18
26
|
*/
|
|
19
27
|
export default function SectionCta({
|
|
28
|
+
variant = 'editorial',
|
|
29
|
+
headline,
|
|
30
|
+
body,
|
|
31
|
+
actions,
|
|
20
32
|
eyebrow,
|
|
21
33
|
promptLabel,
|
|
22
34
|
heading,
|
|
@@ -25,6 +37,27 @@ export default function SectionCta({
|
|
|
25
37
|
secondaryRows = [],
|
|
26
38
|
className = '',
|
|
27
39
|
}) {
|
|
40
|
+
if (variant === 'centered') {
|
|
41
|
+
return (
|
|
42
|
+
<section className={`w-full py-24 ${className}`.trim()}>
|
|
43
|
+
<div className="max-w-[900px] mx-auto">
|
|
44
|
+
<div className="w-32 h-px bg-fg-24 mx-auto mb-8" />
|
|
45
|
+
<SectionText
|
|
46
|
+
align="center"
|
|
47
|
+
headline={headline}
|
|
48
|
+
headlineSize="heading-02"
|
|
49
|
+
headlineClass="kol-sans-heading-02 text-auto"
|
|
50
|
+
body={body}
|
|
51
|
+
bodyClass="kol-mono-14 text-fg-64 max-w-[600px] mx-auto"
|
|
52
|
+
actions={actions}
|
|
53
|
+
actionsClass="pt-4 flex flex-col sm:flex-row gap-4 justify-center"
|
|
54
|
+
gap="gap-8"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
</section>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
28
61
|
const rows = [
|
|
29
62
|
...(promptLabel || heading ? [{ label: promptLabel, value: heading }] : []),
|
|
30
63
|
...secondaryRows,
|
|
@@ -5,10 +5,19 @@ import OverlayGlassPanel from '../utilities/OverlayGlassPanel.jsx'
|
|
|
5
5
|
import AssetPlaceholder from '../utilities/AssetPlaceholder.jsx'
|
|
6
6
|
import ContentMedia from '../molecules/ContentMedia.jsx'
|
|
7
7
|
import SectionText, { HEADLINE_ROLE } from '../molecules/SectionText.jsx'
|
|
8
|
+
import FeaturedCarousel from './FeaturedCarousel.jsx'
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
/* Height presets (SectionHeroRound2, user ruling 2026-08-26): three tiers in
|
|
11
|
+
* viewport units, each with a phone value the DS picks. `full` sits UNDER a
|
|
12
|
+
* fixed navbar — the bar floats over the media — so consumers drop their
|
|
13
|
+
* pt-14 wrapper. dvh, not vh: a phone's URL bar would otherwise push the
|
|
14
|
+
* hero's foot off screen. `lg` / `md` / `screen` stay as aliases; anything
|
|
15
|
+
* else is passed through as a class string. */
|
|
10
16
|
const HEIGHTS = {
|
|
11
|
-
|
|
17
|
+
full: 'h-dvh',
|
|
18
|
+
80: 'h-[70svh] md:h-[80vh]',
|
|
19
|
+
60: 'h-[50svh] md:h-[60vh]',
|
|
20
|
+
screen: 'h-dvh',
|
|
12
21
|
lg: 'h-[440px] md:h-[640px]',
|
|
13
22
|
md: 'h-[320px] md:h-[440px]',
|
|
14
23
|
}
|
|
@@ -76,10 +85,25 @@ function MediaLayer({ media }) {
|
|
|
76
85
|
* `align` picks the media side there ('left' default · 'right'); below `md`
|
|
77
86
|
* the halves stack, media first.
|
|
78
87
|
*
|
|
88
|
+
* ROUND 2 (SectionHeroRound2, kol-website 2026-08-26 — "maintain ONE hero
|
|
89
|
+
* component"): `height` presets `full` · `80` · `60` in viewport units;
|
|
90
|
+
* `justify="end"` pins the content to the foot (StackHero's items-end +
|
|
91
|
+
* pb ramp); `veil` lays the bottom-heavy gradient over the media (the same
|
|
92
|
+
* device as the split's caption veil); `foot` + `overlap` render a node
|
|
93
|
+
* across the fold with the negative margin owned here; `media` as an ARRAY
|
|
94
|
+
* turns the hero into the carousel (FeaturedCarousel is the engine — one
|
|
95
|
+
* glass panel per slide, autoplay, prev/next); and NO media renders the
|
|
96
|
+
* composed text on the surface with no glass panel — the text-only hero.
|
|
97
|
+
*
|
|
79
98
|
* @param {'media'|'split'} variant
|
|
80
|
-
* @param {ReactNode|{src, kind, poster, srcSet, alt}} media background (media) / the half (split)
|
|
99
|
+
* @param {ReactNode|{src, kind, poster, srcSet, alt}|Array} media background (media) / the half (split) / an ARRAY of slides → carousel
|
|
81
100
|
* @param {number} overlayOpacity 0–100 surface-primary scrim over the media (default 0)
|
|
82
|
-
* @param {
|
|
101
|
+
* @param {boolean} veil bottom-heavy gradient over the media
|
|
102
|
+
* @param {'center'|'end'} justify content vertically centred, or pinned to the foot
|
|
103
|
+
* @param {ReactNode} foot a node rendered across the hero's bottom edge
|
|
104
|
+
* @param {number} overlap how far `foot` rises into the hero, px (default 250)
|
|
105
|
+
* @param {boolean} autoPlay · {number} autoPlayInterval · {'stack'|'header'} navPosition carousel only
|
|
106
|
+
* @param {string} height 'full' | '80' | '60' (viewport tiers) · 'lg' | 'md' | 'screen' (aliases) · or a height class string
|
|
83
107
|
* @param {ReactNode} label · headline · body · actions the composed text (SectionText)
|
|
84
108
|
* @param {string} [headlineSize='display-04']
|
|
85
109
|
* @param {string} [panelMaxWidth='max-w-[440px]'] the glass panel's cap on the composed route
|
|
@@ -98,6 +122,13 @@ export default function SectionHero({
|
|
|
98
122
|
fullBleed = false,
|
|
99
123
|
media,
|
|
100
124
|
overlayOpacity = 0,
|
|
125
|
+
veil = false,
|
|
126
|
+
justify = 'center',
|
|
127
|
+
foot,
|
|
128
|
+
overlap = 250,
|
|
129
|
+
autoPlay = false,
|
|
130
|
+
autoPlayInterval = 5000,
|
|
131
|
+
navPosition = 'stack',
|
|
101
132
|
height = 'lg',
|
|
102
133
|
label,
|
|
103
134
|
headline,
|
|
@@ -120,7 +151,7 @@ export default function SectionHero({
|
|
|
120
151
|
? <ContentMedia ratio={null} radius={false} fit="cover" className="h-full">{media}</ContentMedia>
|
|
121
152
|
: <MediaLayer media={media} />
|
|
122
153
|
return (
|
|
123
|
-
<section className={`kol-section-hero-split grid
|
|
154
|
+
<section className={`kol-section-hero-split grid grid-cols-1 md:grid-cols-2 ${(HEIGHTS[height === 'lg' ? 'full' : height] || height).replace(/\bh-/g, 'min-h-')} ${bleed} ${className}`.replace(/\s+/g, ' ').trim()}>
|
|
124
155
|
<div className={`relative min-h-[50vh] overflow-hidden md:min-h-0 ${mediaFirst ? '' : 'md:order-2'}`.trim()}>{mediaNode}</div>
|
|
125
156
|
<div className={`flex items-center justify-center p-10 ${mediaFirst ? '' : 'md:order-1'}`.trim()}>
|
|
126
157
|
<SectionText
|
|
@@ -148,27 +179,70 @@ export default function SectionHero({
|
|
|
148
179
|
: align === 'end' ? 'justify-end'
|
|
149
180
|
: 'justify-center'
|
|
150
181
|
const composed = label || headline || body || actions
|
|
151
|
-
const content = panel ?? (composed ? (
|
|
152
|
-
<OverlayGlassPanel maxWidth={panelMaxWidth} align={align === 'center' ? 'center' : 'start'} {...panelProps}>
|
|
153
|
-
<SectionText
|
|
154
|
-
label={label}
|
|
155
|
-
headline={headline}
|
|
156
|
-
headlineSize={headlineSize}
|
|
157
|
-
body={body}
|
|
158
|
-
actions={actions}
|
|
159
|
-
align={align === 'center' ? 'center' : 'start'}
|
|
160
|
-
/* gap-6 — the rhythm every hand-built OverlayGlassPanel hero ran on,
|
|
161
|
-
* so the composed route lands on the same pixels */
|
|
162
|
-
gap="gap-6"
|
|
163
|
-
labelClass="kol-helper-12 text-meta"
|
|
164
|
-
bodyClass="kol-sans-body-02 text-body"
|
|
165
|
-
actionsClass={`flex flex-wrap gap-4${align === 'center' ? ' justify-center' : ''}`}
|
|
166
|
-
/>
|
|
167
|
-
</OverlayGlassPanel>
|
|
168
|
-
) : children)
|
|
169
182
|
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
/* the foot straddles the fold: the organism owns the negative margin */
|
|
184
|
+
const withFoot = (hero) =>
|
|
185
|
+
foot ? (
|
|
186
|
+
<div className="kol-section-hero-wrap relative">
|
|
187
|
+
{hero}
|
|
188
|
+
<div className="relative z-20" style={{ marginTop: `-${overlap}px` }}>{foot}</div>
|
|
189
|
+
</div>
|
|
190
|
+
) : hero
|
|
191
|
+
|
|
192
|
+
/* CAROUSEL — an array of slides. FeaturedCarousel is the engine (embla,
|
|
193
|
+
* autoplay ring, one glass panel per slide); the hero is its one home. A
|
|
194
|
+
* slide is `{ src, kind, poster, alt, title, subtitle, description, href,
|
|
195
|
+
* ctaLabel }` or the carousel's own `{ media, … }` shape. */
|
|
196
|
+
if (Array.isArray(media)) {
|
|
197
|
+
const items = media.map((s) => (s.media ? s : { ...s, media: { src: s.src, kind: s.kind, poster: s.poster, srcSet: s.srcSet, alt: s.alt } }))
|
|
198
|
+
return withFoot(
|
|
199
|
+
<FeaturedCarousel
|
|
200
|
+
items={items}
|
|
201
|
+
fullWidth
|
|
202
|
+
rounded={false}
|
|
203
|
+
showHeader={false}
|
|
204
|
+
height={heightCls}
|
|
205
|
+
autoPlay={autoPlay}
|
|
206
|
+
autoPlayInterval={autoPlayInterval}
|
|
207
|
+
navPosition={navPosition}
|
|
208
|
+
className={`kol-section-hero-carousel ${fullBleed ? 'w-screen ml-[calc(50%-50vw)]' : ''} ${className}`.replace(/\s+/g, ' ').trim()}
|
|
209
|
+
>
|
|
210
|
+
{children}
|
|
211
|
+
</FeaturedCarousel>,
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const text = composed ? (
|
|
216
|
+
<SectionText
|
|
217
|
+
label={label}
|
|
218
|
+
headline={headline}
|
|
219
|
+
headlineSize={headlineSize}
|
|
220
|
+
body={body}
|
|
221
|
+
actions={actions}
|
|
222
|
+
align={align === 'center' ? 'center' : 'start'}
|
|
223
|
+
/* gap-6 — the rhythm every hand-built OverlayGlassPanel hero ran on,
|
|
224
|
+
* so the composed route lands on the same pixels */
|
|
225
|
+
gap="gap-6"
|
|
226
|
+
labelClass="kol-helper-12 text-meta"
|
|
227
|
+
bodyClass="kol-sans-body-02 text-body"
|
|
228
|
+
actionsClass={`flex flex-wrap gap-4${align === 'center' ? ' justify-center' : ''}`}
|
|
229
|
+
className={media == null ? `${panelMaxWidth} mx-auto` : ''}
|
|
230
|
+
/>
|
|
231
|
+
) : null
|
|
232
|
+
/* no media → the TEXT-ONLY hero: the composed text on the surface, no glass
|
|
233
|
+
* (FoundryLicensing's hero); with media, the glass panel over it */
|
|
234
|
+
const content = panel ?? (composed
|
|
235
|
+
? (media == null ? text : (
|
|
236
|
+
<OverlayGlassPanel maxWidth={panelMaxWidth} align={align === 'center' ? 'center' : 'start'} {...panelProps}>
|
|
237
|
+
{text}
|
|
238
|
+
</OverlayGlassPanel>
|
|
239
|
+
))
|
|
240
|
+
: children)
|
|
241
|
+
|
|
242
|
+
const justifyCls = justify === 'end' ? 'items-end pb-32 sm:pb-40 lg:pb-48 xl:pb-56' : 'items-center'
|
|
243
|
+
|
|
244
|
+
return withFoot(
|
|
245
|
+
<section className={`kol-full-bleed-hero relative isolate w-full overflow-hidden ${heightCls} ${fullBleed ? 'w-screen ml-[calc(50%-50vw)]' : ''} ${className}`.replace(/\s+/g, ' ').trim()}>
|
|
172
246
|
<MediaLayer media={media} />
|
|
173
247
|
{overlayOpacity > 0 && (
|
|
174
248
|
<div
|
|
@@ -177,9 +251,10 @@ export default function SectionHero({
|
|
|
177
251
|
style={{ background: 'var(--kol-surface-primary)', opacity: overlayOpacity / 100 }}
|
|
178
252
|
/>
|
|
179
253
|
)}
|
|
180
|
-
<div className=
|
|
254
|
+
{veil && <div aria-hidden="true" className="kol-section-hero-veil absolute inset-0" />}
|
|
255
|
+
<div className={`relative z-10 flex h-full w-full ${justifyCls} ${alignCls} p-6 md:p-10`}>
|
|
181
256
|
{content}
|
|
182
257
|
</div>
|
|
183
|
-
</section
|
|
258
|
+
</section>,
|
|
184
259
|
)
|
|
185
260
|
}
|