@kolkrabbi/kol-component 0.74.2 → 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,13 +85,31 @@ 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
|
|
110
|
+
* @param {object} panelProps everything else on the glass panel (user ask 2026-08-26):
|
|
111
|
+
* `padding` ('px-6 py-8') · `maxWidth` · `surfaceOpacity`
|
|
112
|
+
* (80) · `blur` ('1px') · `gap` · `className`
|
|
86
113
|
* @param {ReactNode} panel a caller-authored content node; wins over the text props and children
|
|
87
114
|
* @param {ReactNode} children fallback content slot
|
|
88
115
|
* @param {string} align media: 'center' | 'start' | 'end' — placement of the content ·
|
|
@@ -95,6 +122,13 @@ export default function SectionHero({
|
|
|
95
122
|
fullBleed = false,
|
|
96
123
|
media,
|
|
97
124
|
overlayOpacity = 0,
|
|
125
|
+
veil = false,
|
|
126
|
+
justify = 'center',
|
|
127
|
+
foot,
|
|
128
|
+
overlap = 250,
|
|
129
|
+
autoPlay = false,
|
|
130
|
+
autoPlayInterval = 5000,
|
|
131
|
+
navPosition = 'stack',
|
|
98
132
|
height = 'lg',
|
|
99
133
|
label,
|
|
100
134
|
headline,
|
|
@@ -102,6 +136,7 @@ export default function SectionHero({
|
|
|
102
136
|
body,
|
|
103
137
|
actions,
|
|
104
138
|
panelMaxWidth = 'max-w-[440px]',
|
|
139
|
+
panelProps,
|
|
105
140
|
panel,
|
|
106
141
|
children,
|
|
107
142
|
align = 'center',
|
|
@@ -116,7 +151,7 @@ export default function SectionHero({
|
|
|
116
151
|
? <ContentMedia ratio={null} radius={false} fit="cover" className="h-full">{media}</ContentMedia>
|
|
117
152
|
: <MediaLayer media={media} />
|
|
118
153
|
return (
|
|
119
|
-
<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()}>
|
|
120
155
|
<div className={`relative min-h-[50vh] overflow-hidden md:min-h-0 ${mediaFirst ? '' : 'md:order-2'}`.trim()}>{mediaNode}</div>
|
|
121
156
|
<div className={`flex items-center justify-center p-10 ${mediaFirst ? '' : 'md:order-1'}`.trim()}>
|
|
122
157
|
<SectionText
|
|
@@ -144,27 +179,70 @@ export default function SectionHero({
|
|
|
144
179
|
: align === 'end' ? 'justify-end'
|
|
145
180
|
: 'justify-center'
|
|
146
181
|
const composed = label || headline || body || actions
|
|
147
|
-
const content = panel ?? (composed ? (
|
|
148
|
-
<OverlayGlassPanel maxWidth={panelMaxWidth} align={align === 'center' ? 'center' : 'start'}>
|
|
149
|
-
<SectionText
|
|
150
|
-
label={label}
|
|
151
|
-
headline={headline}
|
|
152
|
-
headlineSize={headlineSize}
|
|
153
|
-
body={body}
|
|
154
|
-
actions={actions}
|
|
155
|
-
align={align === 'center' ? 'center' : 'start'}
|
|
156
|
-
/* gap-6 — the rhythm every hand-built OverlayGlassPanel hero ran on,
|
|
157
|
-
* so the composed route lands on the same pixels */
|
|
158
|
-
gap="gap-6"
|
|
159
|
-
labelClass="kol-helper-12 text-meta"
|
|
160
|
-
bodyClass="kol-sans-body-02 text-body"
|
|
161
|
-
actionsClass={`flex flex-wrap gap-4${align === 'center' ? ' justify-center' : ''}`}
|
|
162
|
-
/>
|
|
163
|
-
</OverlayGlassPanel>
|
|
164
|
-
) : children)
|
|
165
182
|
|
|
166
|
-
|
|
167
|
-
|
|
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()}>
|
|
168
246
|
<MediaLayer media={media} />
|
|
169
247
|
{overlayOpacity > 0 && (
|
|
170
248
|
<div
|
|
@@ -173,9 +251,10 @@ export default function SectionHero({
|
|
|
173
251
|
style={{ background: 'var(--kol-surface-primary)', opacity: overlayOpacity / 100 }}
|
|
174
252
|
/>
|
|
175
253
|
)}
|
|
176
|
-
<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`}>
|
|
177
256
|
{content}
|
|
178
257
|
</div>
|
|
179
|
-
</section
|
|
258
|
+
</section>,
|
|
180
259
|
)
|
|
181
260
|
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* @param {string} align 'center' | 'start' | 'end' (cross-axis + text)
|
|
12
12
|
* @param {string} gap vertical rhythm class between children
|
|
13
13
|
* @param {string} maxWidth optional max-w-* class (adds mx-auto)
|
|
14
|
+
* @param {string} padding padding classes (default 'px-6 py-8') — 2026-08-26,
|
|
15
|
+
* the one knob the panel had no seam for
|
|
14
16
|
*/
|
|
15
17
|
export default function OverlayGlassPanel({
|
|
16
18
|
children,
|
|
@@ -19,6 +21,7 @@ export default function OverlayGlassPanel({
|
|
|
19
21
|
align = 'center',
|
|
20
22
|
gap = 'gap-6',
|
|
21
23
|
maxWidth = '',
|
|
24
|
+
padding = 'px-6 py-8',
|
|
22
25
|
className = '',
|
|
23
26
|
}) {
|
|
24
27
|
const alignCls =
|
|
@@ -28,7 +31,7 @@ export default function OverlayGlassPanel({
|
|
|
28
31
|
|
|
29
32
|
return (
|
|
30
33
|
<div
|
|
31
|
-
className={`flex flex-col ${alignCls} ${gap} rounded-[var(--kol-radius-xs)]
|
|
34
|
+
className={`flex flex-col ${alignCls} ${gap} rounded-[var(--kol-radius-xs)] ${padding} ${maxWidth ? `${maxWidth} mx-auto` : ''} ${className}`.trim()}
|
|
32
35
|
style={{
|
|
33
36
|
backgroundColor: `color-mix(in srgb, var(--kol-surface-primary) ${surfaceOpacity}%, transparent)`,
|
|
34
37
|
backdropFilter: `blur(${blur})`,
|