@kolkrabbi/kol-component 0.90.2 → 0.91.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.91.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",
|
|
@@ -91,6 +91,8 @@ const MEDIA = {
|
|
|
91
91
|
|
|
92
92
|
export default function ContentCard({
|
|
93
93
|
variant = 'default',
|
|
94
|
+
hero = false,
|
|
95
|
+
label,
|
|
94
96
|
pad,
|
|
95
97
|
media,
|
|
96
98
|
ratio,
|
|
@@ -110,10 +112,31 @@ export default function ContentCard({
|
|
|
110
112
|
...text
|
|
111
113
|
}) {
|
|
112
114
|
const box = BOX[variant] ?? BOX.default
|
|
115
|
+
/* HERO (2026-08-27 — the featured card riding a page's fold; ListingCard
|
|
116
|
+
* size="hero" had no ContentCard equivalent, which is why Stack still
|
|
117
|
+
* imported it): a header row above the media — `label` left, `meta` chips
|
|
118
|
+
* right — the media on the zoom's hero rung (1.02), the text on
|
|
119
|
+
* ContentText's `hero` form (display-03 title). article only; the row form
|
|
120
|
+
* has no hero. */
|
|
121
|
+
const isHero = hero && variant === 'article'
|
|
122
|
+
const { meta: heroMeta, ...textSlots } = text
|
|
123
|
+
if (!isHero) Object.assign(textSlots, { meta: text.meta })
|
|
124
|
+
if (isHero) delete textSlots.tags
|
|
125
|
+
const metaChips = isHero && heroMeta != null ? (Array.isArray(heroMeta) ? heroMeta : [heroMeta]) : null
|
|
126
|
+
const heroHeader = isHero && (label != null || metaChips?.length) ? (
|
|
127
|
+
<div className="kol-card-hero-header flex items-center justify-between" style={{ marginBottom: 'var(--kol-spacing-4)' }}>
|
|
128
|
+
{label != null && <div className="kol-helper-14 text-meta">{label}</div>}
|
|
129
|
+
{metaChips?.length > 0 && (
|
|
130
|
+
<div className="flex gap-3 kol-helper-12 text-meta">
|
|
131
|
+
{metaChips.map((item, i) => <span key={i}>{item}</span>)}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
) : null
|
|
113
136
|
const r = ratio ?? RATIOS[variant]
|
|
114
137
|
const padding = pad ? `var(--kol-pad-card-${pad})` : box.pad
|
|
115
138
|
/* image-only cards (print) pass no text slots — the empty plate must not render */
|
|
116
|
-
const hasText = ['title', 'body', 'kicker', 'detail', 'date', 'size', 'meta', 'tags'].some((k) =>
|
|
139
|
+
const hasText = ['title', 'body', 'kicker', 'detail', 'date', 'size', 'meta', 'tags'].some((k) => textSlots[k] != null)
|
|
117
140
|
/* `actions` sit IN THE TEXT PLATE, bottom-right (user ruling 2026-08-15:
|
|
118
141
|
* *"space in text bottom right"*). Not stacked under the copy in their own
|
|
119
142
|
* row — that grew the card — and not on the media, which was my call to make
|
|
@@ -138,7 +161,7 @@ export default function ContentCard({
|
|
|
138
161
|
zIndex: box.layout === 'canvas' ? 1 : undefined,
|
|
139
162
|
}}
|
|
140
163
|
>
|
|
141
|
-
{hasText && <ContentText variant={variant} form=
|
|
164
|
+
{hasText && <ContentText variant={variant} form={isHero ? 'hero' : 'card'} {...textSlots} />}
|
|
142
165
|
{/* ABSOLUTE, not a flex sibling: the plate's height moves with the title
|
|
143
166
|
* and the meta, so a laid-out stack would stretch or drift with it. The
|
|
144
167
|
* inset reads the SAME pad token the plate uses, so the icons sit the
|
|
@@ -166,7 +189,7 @@ export default function ContentCard({
|
|
|
166
189
|
frame: frame ?? MEDIA[variant]?.frame ?? false,
|
|
167
190
|
ring: ring ?? MEDIA[variant]?.ring ?? false,
|
|
168
191
|
borderHover: MEDIA[variant]?.borderHover ?? false,
|
|
169
|
-
zoom: zoom ?? MEDIA[variant]?.zoom ?? false,
|
|
192
|
+
zoom: zoom ?? (isHero ? 'hero' : MEDIA[variant]?.zoom ?? false),
|
|
170
193
|
}
|
|
171
194
|
|
|
172
195
|
/* `control` — the in-frame control slot (user ruling 2026-08-15). One node,
|
|
@@ -181,6 +204,7 @@ export default function ContentCard({
|
|
|
181
204
|
const body =
|
|
182
205
|
box.layout === 'stack' ? (
|
|
183
206
|
<>
|
|
207
|
+
{heroHeader}
|
|
184
208
|
<div className="relative">
|
|
185
209
|
<ContentMedia ratio={r} {...mediaProps}>{media}</ContentMedia>
|
|
186
210
|
{controlNode}
|
|
@@ -241,6 +265,7 @@ export default function ContentCard({
|
|
|
241
265
|
const hoverBg = HOVER[variant]
|
|
242
266
|
|
|
243
267
|
const common = {
|
|
268
|
+
'data-tags': isHero && Array.isArray(text.tags) && text.tags.length ? text.tags.join(' ') : undefined,
|
|
244
269
|
className: `kol-card group flex ${expanded ? 'flex-row-reverse' : 'flex-col'} ${box.layout === 'drawer' ? 'relative overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${framed ? 'overflow-hidden rounded-[var(--kol-radius-sm)]' : ''} ${box.border ? 'border' : ''} ${box.layout === 'canvas' ? 'relative' : ''} ${interactive ? 'cursor-pointer select-none' : ''} ${hoverBg && interactive ? 'kol-content-hover' : ''} ${className}`.trim(),
|
|
245
270
|
style: {
|
|
246
271
|
/* same reason as ContentRow: rest colours are PROPERTIES, because an
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* @param {ReactNode} kicker article only
|
|
21
21
|
* @param {ReactNode} detail catalog · print
|
|
22
22
|
* @param {ReactNode} date default · article · typeface
|
|
23
|
+
* @param {'card'|'row'|'hero'} form the block's form; `hero` is article's featured text (ContentCard `hero`)
|
|
23
24
|
* @param {ReactNode} size default · article (file size / read length)
|
|
24
25
|
* @param {ReactNode} meta work only
|
|
25
26
|
* @param {number} gap inner line gap in px (defaults per variant/form)
|
|
@@ -64,6 +65,11 @@ const RAMP = {
|
|
|
64
65
|
/* kol-content-title-dim: the title dims to 70% on card hover (StackCardHover, 2026-08-27 — ListingCard's move, kol-theme) */
|
|
65
66
|
card: { kicker: 'kol-mono-12 text-meta', title: 'kol-sans-heading-03 text-emphasis kol-content-title-dim', body: 'kol-mono-14 text-body', date: 'kol-mono-12 text-meta', size: 'kol-mono-12 text-meta', tags: 'flex flex-wrap gap-2' },
|
|
66
67
|
row: { kicker: 'kol-mono-12 text-meta', title: 'kol-sans-heading-04 text-emphasis kol-content-title-dim', body: 'kol-mono-12 text-body', date: 'kol-mono-12 text-meta', size: 'kol-mono-12 text-meta', tags: 'flex flex-wrap gap-2' },
|
|
68
|
+
/* HERO — the featured card riding a page's fold (ContentCard `hero`,
|
|
69
|
+
* 2026-08-27): ListingCard size="hero"'s text, read off Stack — kicker in
|
|
70
|
+
* the helper-16 voice, title on display-03 (Stack's ruled hero title),
|
|
71
|
+
* body mono-14. One emphasis (the title), everything else meta/body. */
|
|
72
|
+
hero: { kicker: 'kol-helper-16 text-meta', title: 'kol-sans-display-03 text-emphasis kol-content-title-dim', body: 'kol-mono-14 text-body', date: 'kol-mono-12 text-meta', size: 'kol-mono-12 text-meta', tags: 'flex flex-wrap gap-2' },
|
|
67
73
|
},
|
|
68
74
|
work: {
|
|
69
75
|
/* INVERSE ink — the card's plate is the drawer, `surface-inverse`. Leaving
|
|
@@ -111,6 +117,8 @@ const ORDER = {
|
|
|
111
117
|
* form's own outer gap. A flat column gave every line the same gap, which
|
|
112
118
|
* read as unrelated lines rather than a heading with its standfirst. */
|
|
113
119
|
article: { card: ['tags', 'kicker', ['stack', 'title', 'body'], ['group', 'date', 'size']], row: ['kicker', ['stack', 'title', 'body'], ['group', 'date', 'size']] },
|
|
120
|
+
/* the hero carries its tags as data (ListingCard hero: `data-tags` only) and its meta in the header row above the media — neither is a text line here */
|
|
121
|
+
articleHero: ['kicker', ['stack', 'title', 'body'], ['group', 'date', 'size']],
|
|
114
122
|
/* work CARD = the drawer's two lines: title, then one meta line.
|
|
115
123
|
*
|
|
116
124
|
* work ROW = WorkListItem, read off the live /work listing: a LEFT column of
|
|
@@ -155,7 +163,7 @@ const GAPS = {
|
|
|
155
163
|
default: { card: 'var(--kol-spacing-3)', row: 'var(--kol-spacing-2)' },
|
|
156
164
|
catalog: { card: 'var(--kol-spacing-1)', row: 'var(--kol-spacing-2)' },
|
|
157
165
|
print: { card: 'var(--kol-spacing-2)', row: 'var(--kol-spacing-2)' },
|
|
158
|
-
article: { card: 'var(--kol-spacing-3)', row: 'var(--kol-spacing-3)' },
|
|
166
|
+
article: { card: 'var(--kol-spacing-3)', row: 'var(--kol-spacing-3)', hero: 'var(--kol-spacing-3)' },
|
|
159
167
|
work: { card: 'var(--kol-spacing-2)', row: 'var(--kol-spacing-4)' },
|
|
160
168
|
typeface: { card: 'var(--kol-spacing-2)', row: 'var(--kol-spacing-6)' },
|
|
161
169
|
}
|
|
@@ -169,7 +177,7 @@ export default function ContentText({
|
|
|
169
177
|
className = '',
|
|
170
178
|
}) {
|
|
171
179
|
const ramp = RAMP[variant]?.[form] ?? RAMP.default[form] ?? RAMP.default.card
|
|
172
|
-
const order = ORDER[variant]?.[form]
|
|
180
|
+
const order = (form === 'hero' && ORDER[`${variant}Hero`]) || ORDER[variant]?.[form] || ORDER.default.card
|
|
173
181
|
const values = { title, body, kicker, detail, date, size, meta, tags }
|
|
174
182
|
const overrides = { title: titleClass, body: bodyClass, kicker: kickerClass, detail: detailClass, date: dateClass, size: sizeClass, meta: metaClass, tags: tagsClass }
|
|
175
183
|
|