@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.75.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
- * FoundryCTAthe simple centered tier of the CTA-band family: a centered
7
- * column with a short rule line, a heading, a mono description, and one or
8
- * more Button actions. CtaGlobal is the editorial two-column closer of the
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
- * Presentational copy arrives as props and renders verbatim (no
12
- * text-transform; author strings in their final case). Actions with an
13
- * `href` render Button's anchor form; pass `onNavigate` for SPA routing —
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
- <section className={`w-full py-24 ${className}`.trim()}>
36
- <div className="max-w-[900px] mx-auto text-center space-y-8">
37
- <div className="w-32 h-px bg-fg-24 mx-auto" />
38
-
39
- {heading && <h2 className="kol-sans-heading-02 text-auto">{heading}</h2>}
40
-
41
- {description && (
42
- <p className="kol-mono-14 text-fg-64 max-w-[600px] mx-auto">{description}</p>
43
- )}
44
-
45
- {actions.length > 0 && (
46
- <div className={`pt-4 ${actions.length > 1 ? 'flex flex-col sm:flex-row gap-4 justify-center' : ''}`.trim()}>
47
- {actions.map((act, i) => (
48
- <Button
49
- key={i}
50
- variant={act.variant === 'secondary' ? 'outline' : 'primary'}
51
- size="md"
52
- href={act.href}
53
- target={act.target}
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
- /** Height presets; anything else is passed through as a class string. */
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
- screen: 'h-screen',
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 {string} height 'screen' | 'lg' | 'md' preset, or a height class string
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 min-h-screen grid-cols-1 md:grid-cols-2 ${bleed} ${className}`.replace(/\s+/g, ' ').trim()}>
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
- return (
171
- <section className={`kol-full-bleed-hero relative isolate w-full overflow-hidden ${heightCls} ${className}`.trim()}>
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={`relative z-10 flex h-full w-full items-center ${alignCls} p-6 md:p-10`}>
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
  }