@kolkrabbi/kol-component 0.78.0 → 0.80.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.78.0",
3
+ "version": "0.80.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",
@@ -44,6 +44,8 @@ export default function SectionCardItem({
44
44
  onNavigate,
45
45
  imageAspectRatio = 'auto',
46
46
  imagePosition = 'center',
47
+ className = '',
48
+ style,
47
49
  }) {
48
50
  const isSvgUrl = typeof visual === 'string' && visual.endsWith('.svg')
49
51
 
@@ -104,7 +106,7 @@ export default function SectionCardItem({
104
106
  </>
105
107
  )
106
108
 
107
- const baseClasses = `kol-card-feature w-full flex-1 h-[304px] md:h-72 p-4 md:p-6 gap-4 ${backgroundColor} rounded border border-fg-08 flex flex-col justify-between items-start overflow-hidden`
109
+ const baseClasses = `kol-card-feature w-full flex-1 h-[304px] md:h-72 p-4 md:p-6 gap-4 ${backgroundColor} rounded border border-fg-08 flex flex-col justify-between items-start overflow-hidden ${className}`.trim()
108
110
 
109
111
  if (href) {
110
112
  const isExternal = href.startsWith('http') || href.startsWith('mailto')
@@ -114,6 +116,7 @@ export default function SectionCardItem({
114
116
  <a
115
117
  href={href}
116
118
  className={`${baseClasses} hover:border-fg-32 transition-colors duration-300`}
119
+ style={style}
117
120
  target="_blank"
118
121
  rel="noreferrer noopener"
119
122
  >
@@ -127,11 +130,12 @@ export default function SectionCardItem({
127
130
  href={href}
128
131
  onClick={onNavigate}
129
132
  className={`${baseClasses} hover:border-fg-24 transition-colors duration-300`}
133
+ style={style}
130
134
  >
131
135
  {content}
132
136
  </a>
133
137
  )
134
138
  }
135
139
 
136
- return <div className={baseClasses}>{content}</div>
140
+ return <div className={baseClasses} style={style}>{content}</div>
137
141
  }
@@ -21,6 +21,9 @@
21
21
  * @param {ReactNode} headline the heading
22
22
  * @param {'pull'|'display-01'|'display-02'|'display-03'|'display-04'|'heading-01'|'heading-02'|'heading-03'|'heading-04'|'heading-05'} [headlineSize='heading-02']
23
23
  * @param {string} [headlineAs='h2'] element for the headline
24
+ * @param {'auto'|'upper'} [headlineCase='auto'] `upper` = the caps role (`.kol-section-text-caps`) —
25
+ * a title card; the split hero sets it (SectionHeroNoOverrides, 2026-08-26:
26
+ * a ROLE on the molecule, never a hero-side class)
24
27
  * @param {ReactNode} body lede / paragraph(s)
25
28
  * @param {ReactNode} actions row of Buttons / links / Tags
26
29
  * @param {'start'|'center'} [align='start'] text-align + item alignment
@@ -28,6 +31,10 @@
28
31
  * @param {ReactNode} children extra slots rendered between body and actions
29
32
  * (the split's stats strip)
30
33
  * @param {string} labelClass · bodyClass · actionsClass full class overrides (label default: the hero's `kol-helper-12 text-meta`)
34
+ * @param {object} slotClass extra classes per slot — `{ label, headline, body, actions }`
35
+ * (SectionRevealSeams, 2026-08-26: a consumer's reveal system
36
+ * stamps `reveal` on each part; nothing animates in the DS)
37
+ * @param {object} slotStyle inline style per slot — `{ headline: { '--reveal-delay': '0.1s' } }`
31
38
  * @param {string} className extra classes on the block
32
39
  */
33
40
  export const HEADLINE_ROLE = {
@@ -48,6 +55,7 @@ export default function SectionText({
48
55
  headline,
49
56
  headlineSize = 'heading-02',
50
57
  headlineAs: Headline = 'h2',
58
+ headlineCase = 'auto',
51
59
  headlineClass,
52
60
  body,
53
61
  actions,
@@ -61,18 +69,21 @@ export default function SectionText({
61
69
  labelClass = 'kol-helper-12 text-meta',
62
70
  bodyClass = 'kol-section-text-body',
63
71
  actionsClass = 'flex flex-wrap gap-4',
72
+ slotClass = {},
73
+ slotStyle = {},
64
74
  className = '',
65
75
  }) {
76
+ const cls = (base, slot) => `${base} ${slotClass[slot] ?? ''}`.trim()
66
77
  const alignCls = align === 'center' ? 'items-center text-center' : 'items-start text-left'
67
78
  return (
68
79
  <div className={`kol-section-text flex flex-col ${gap} ${alignCls} ${className}`.replace(/\s+/g, ' ').trim()}>
69
80
  {/* `kol-section-text-eyebrow` = uppercase by ROLE (kol-theme ≥0.55.0);
70
81
  * `labelClass` is the voice riding beside it */}
71
- {label && <span className={`kol-section-text-eyebrow ${labelClass}`}>{label}</span>}
72
- {headline && <Headline className={headlineClass ?? (HEADLINE_ROLE[headlineSize] ?? HEADLINE_ROLE['heading-02'])}>{headline}</Headline>}
73
- {body && (typeof body === 'string' ? <p className={bodyClass}>{body}</p> : <div className={bodyClass}>{body}</div>)}
82
+ {label && <span className={cls(`kol-section-text-eyebrow ${labelClass}`, 'label')} style={slotStyle.label}>{label}</span>}
83
+ {headline && <Headline className={cls(`${headlineClass ?? (HEADLINE_ROLE[headlineSize] ?? HEADLINE_ROLE['heading-02'])}${headlineCase === 'upper' ? ' kol-section-text-caps' : ''}`, 'headline')} style={slotStyle.headline}>{headline}</Headline>}
84
+ {body && (typeof body === 'string' ? <p className={cls(bodyClass, 'body')} style={slotStyle.body}>{body}</p> : <div className={cls(bodyClass, 'body')} style={slotStyle.body}>{body}</div>)}
74
85
  {children}
75
- {actions && <div className={actionsClass}>{actions}</div>}
86
+ {actions && <div className={cls(actionsClass, 'actions')} style={slotStyle.actions}>{actions}</div>}
76
87
  </div>
77
88
  )
78
89
  }
@@ -17,6 +17,9 @@ import SectionText, { HEADLINE_ROLE } from '../molecules/SectionText.jsx'
17
17
  * @param {ReactNode} label · headline · body the header (heading-03 + mono lede by default)
18
18
  * @param {ReactNode} actions centred action row under the cards
19
19
  * @param {Function} onNavigate (event, feature) => void
20
+ * @param {string} itemClassName extra classes on every card (reveal seam)
21
+ * @param {object|Function} itemStyle inline style per card, or `(index) => style`
22
+ * @param {object} slotClass · slotStyle per-slot class / style on the header text
20
23
  * @param {string} sectionClassName · wrapperClassName · cardsWrapperClassName · actionsClassName · headerClassName · headerTextWidthClass layout seams
21
24
  */
22
25
  export default function SectionCards({
@@ -27,6 +30,13 @@ export default function SectionCards({
27
30
  body,
28
31
  actions,
29
32
  onNavigate,
33
+ /* the reveal seams (SectionRevealSeams, kol-website 2026-08-26): a
34
+ * consumer's entrance system stamps a class + `--reveal-delay` per card;
35
+ * `itemStyle` is an object or `(index) => object`. Nothing animates here. */
36
+ itemClassName = '',
37
+ itemStyle,
38
+ slotClass,
39
+ slotStyle,
30
40
  sectionClassName = '',
31
41
  /* the family's one cap — the shell's --kol-container-max ladder (2026-08-26) */
32
42
  wrapperClassName = 'w-full flex flex-col gap-8 md:gap-10 max-w-[var(--kol-container-max,var(--kol-content-shell,1800px))] mx-auto',
@@ -51,6 +61,8 @@ export default function SectionCards({
51
61
  bodyClass={`kol-mono-14 text-fg-64 ${headerTextWidthClass}`}
52
62
  labelClass="kol-helper-12 text-meta"
53
63
  gap="gap-3"
64
+ slotClass={slotClass}
65
+ slotStyle={slotStyle}
54
66
  className={headerClassName}
55
67
  />
56
68
  )}
@@ -68,6 +80,8 @@ export default function SectionCards({
68
80
  backgroundColor={feature.backgroundColor}
69
81
  imageAspectRatio={feature.imageAspectRatio}
70
82
  onNavigate={onNavigate ? (event) => onNavigate(event, feature) : undefined}
83
+ className={itemClassName}
84
+ style={typeof itemStyle === 'function' ? itemStyle(index) : itemStyle}
71
85
  />
72
86
  ))}
73
87
  </div>
@@ -35,6 +35,8 @@ export default function SectionCta({
35
35
  contactLabel,
36
36
  email,
37
37
  secondaryRows = [],
38
+ slotClass,
39
+ slotStyle,
38
40
  className = '',
39
41
  }) {
40
42
  if (variant === 'centered') {
@@ -52,6 +54,8 @@ export default function SectionCta({
52
54
  actions={actions}
53
55
  actionsClass="pt-4 flex flex-col sm:flex-row gap-4 justify-center"
54
56
  gap="gap-8"
57
+ slotClass={slotClass}
58
+ slotStyle={slotStyle}
55
59
  />
56
60
  </div>
57
61
  </section>
@@ -82,6 +86,8 @@ export default function SectionCta({
82
86
  headlineAs="div"
83
87
  headlineClass="text-auto kol-sans-heading-01"
84
88
  gap="gap-2"
89
+ slotClass={slotClass}
90
+ slotStyle={slotStyle}
85
91
  className="self-stretch"
86
92
  />
87
93
  ))}
@@ -24,6 +24,8 @@ export default function SectionFaq({
24
24
  items = [],
25
25
  singleOpen = false,
26
26
  defaultOpen,
27
+ slotClass,
28
+ slotStyle,
27
29
  className = '',
28
30
  /* the section sits on the family's cap; the list itself is a READING
29
31
  * column inside it — a measure is a content decision, not the frame */
@@ -35,7 +37,7 @@ export default function SectionFaq({
35
37
  <div className="max-w-[var(--kol-container-max,var(--kol-content-shell,1800px))] mx-auto">
36
38
  <div className={innerClassName}>
37
39
  {(label || headline || body || actions) && (
38
- <SectionText label={label} headline={headline} headlineSize={headlineSize} body={body} actions={actions} />
40
+ <SectionText label={label} headline={headline} headlineSize={headlineSize} body={body} actions={actions} slotClass={slotClass} slotStyle={slotStyle} />
39
41
  )}
40
42
  {items.length > 0 && (
41
43
  <Accordion className="mt-0 mb-0">
@@ -4,7 +4,7 @@ import Image from '../atoms/Image.jsx'
4
4
  import OverlayGlassPanel from '../utilities/OverlayGlassPanel.jsx'
5
5
  import AssetPlaceholder from '../utilities/AssetPlaceholder.jsx'
6
6
  import ContentMedia from '../molecules/ContentMedia.jsx'
7
- import SectionText, { HEADLINE_ROLE } from '../molecules/SectionText.jsx'
7
+ import SectionText from '../molecules/SectionText.jsx'
8
8
  import FeaturedCarousel from './FeaturedCarousel.jsx'
9
9
 
10
10
  /* Height presets (SectionHeroRound2, user ruling 2026-08-26): three tiers in
@@ -109,7 +109,9 @@ function MediaLayer({ media }) {
109
109
  * slide's title in the typeface's own font and routes the CTA through the SPA)
110
110
  * @param {string} height 'full' | '80' | '60' (viewport tiers) · 'lg' | 'md' | 'screen' (aliases) · or a height class string
111
111
  * @param {ReactNode} label · headline · body · actions the composed text (SectionText)
112
- * @param {string} [headlineSize='display-04']
112
+ * @param {object} slotClass · slotStyle per-slot class / style, forwarded to SectionText (a consumer's reveal seam)
113
+ * @param {string} headlineSize role; defaults per variant — media 'display-04', split 'heading-02'
114
+ * @param {string} gap SectionText's inner rhythm, forwarded as given (no hero default)
113
115
  * @param {string} [panelMaxWidth='max-w-[440px]'] the glass panel's cap on the composed route
114
116
  * @param {object} panelProps everything else on the glass panel (user ask 2026-08-26):
115
117
  * `padding` ('px-6 py-8') · `maxWidth` · `surfaceOpacity`
@@ -145,11 +147,17 @@ export default function SectionHero({
145
147
  height = 'lg',
146
148
  label,
147
149
  headline,
148
- headlineSize = 'display-04',
150
+ /* per-variant DEFAULT only — the hero has no say in the text's voice
151
+ * (SectionHeroNoOverrides, 2026-08-26): media 'display-04', split
152
+ * 'heading-02'; the consumer's value wins */
153
+ headlineSize,
154
+ gap,
149
155
  body,
150
156
  actions,
151
157
  panelMaxWidth = 'max-w-[440px]',
152
158
  panelProps,
159
+ slotClass,
160
+ slotStyle,
153
161
  panel,
154
162
  children,
155
163
  align = 'center',
@@ -167,18 +175,20 @@ export default function SectionHero({
167
175
  <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()}>
168
176
  <div className={`relative min-h-[50vh] overflow-hidden md:min-h-0 ${mediaFirst ? '' : 'md:order-2'}`.trim()}>{mediaNode}</div>
169
177
  <div className={`flex items-center justify-center p-10 ${mediaFirst ? '' : 'md:order-1'}`.trim()}>
178
+ {/* BARE — the molecule's own voice; `headlineCase="upper"` is the
179
+ * split hero's one trait, a role on SectionText (SectionHeroNoOverrides) */}
170
180
  <SectionText
171
181
  label={label}
172
182
  headline={headline}
173
- headlineSize={headlineSize === 'display-04' ? 'heading-02' : headlineSize}
174
- headlineClass={`${HEADLINE_ROLE[headlineSize === 'display-04' ? 'heading-02' : headlineSize] ?? HEADLINE_ROLE['heading-02']} kol-section-text-caps`}
183
+ headlineSize={headlineSize ?? 'heading-02'}
184
+ headlineCase="upper"
175
185
  body={body}
176
186
  actions={actions}
177
187
  align="center"
178
- gap="gap-6"
179
- bodyClass="kol-sans-body-02 text-body"
180
- actionsClass="flex flex-wrap gap-4 justify-center"
188
+ gap={gap}
181
189
  className="max-w-[var(--kol-content-column)]"
190
+ slotClass={slotClass}
191
+ slotStyle={slotStyle}
182
192
  />
183
193
  </div>
184
194
  </section>
@@ -226,19 +236,23 @@ export default function SectionHero({
226
236
  }
227
237
 
228
238
  const text = composed ? (
239
+ /* BARE (SectionHeroNoOverrides, user 2026-08-26: "you think the purpose
240
+ * of unifying components is to make OVERWRITES?") — the hero passes the
241
+ * slots and nothing about their voice. The old glass hero's gap-6 /
242
+ * body-02 / caps were pixel parity with a retired component, and parity
243
+ * with it is not the bar; one voice across the set is. `gap` is the
244
+ * consumer's if they want it. */
229
245
  <SectionText
230
246
  label={label}
231
247
  headline={headline}
232
- headlineSize={headlineSize}
248
+ headlineSize={headlineSize ?? 'display-04'}
233
249
  body={body}
234
250
  actions={actions}
235
251
  align={align === 'center' ? 'center' : 'start'}
236
- /* gap-6 — the rhythm every hand-built OverlayGlassPanel hero ran on,
237
- * so the composed route lands on the same pixels */
238
- gap="gap-6"
239
- bodyClass="kol-sans-body-02 text-body"
240
- actionsClass={`flex flex-wrap gap-4${align === 'center' ? ' justify-center' : ''}`}
252
+ gap={gap}
241
253
  className={media == null ? `${panelMaxWidth} mx-auto` : ''}
254
+ slotClass={slotClass}
255
+ slotStyle={slotStyle}
242
256
  />
243
257
  ) : null
244
258
  /* no media → the TEXT-ONLY hero: the composed text on the surface, no glass
@@ -36,6 +36,7 @@ import SectionText from '../molecules/SectionText.jsx'
36
36
  * @param {ReactNode} caption mono caption + gradient veil over the media
37
37
  * @param {string} bgImage inline cover background on the section
38
38
  * @param {boolean} fullBleed span the full viewport width
39
+ * @param {object} slotClass · slotStyle per-slot class / style on the text block (reveal seam)
39
40
  * @param {string} className · innerClassName · columnClassName layout seams
40
41
  */
41
42
  export default function SectionSplit({
@@ -54,6 +55,8 @@ export default function SectionSplit({
54
55
  caption,
55
56
  bgImage,
56
57
  fullBleed = false,
58
+ slotClass,
59
+ slotStyle,
57
60
  className = '',
58
61
  innerClassName = '',
59
62
  columnClassName = '',
@@ -87,6 +90,8 @@ export default function SectionSplit({
87
90
  actions={actions}
88
91
  actionsClass={`flex flex-wrap gap-4 pt-2${centred ? ' justify-center' : ''}`}
89
92
  align={centred ? 'center' : 'start'}
93
+ slotClass={slotClass}
94
+ slotStyle={slotStyle}
90
95
  className={`max-w-[640px] ${mediaFirst ? 'order-2' : ''} ${columnClassName}`}
91
96
  >
92
97
  {meta && meta.length > 0 && (