@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.74.2",
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,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 {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
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 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()}>
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
- return (
167
- <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()}>
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={`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`}>
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)] px-6 py-8 ${maxWidth ? `${maxWidth} mx-auto` : ''} ${className}`.trim()}
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})`,