@kolkrabbi/kol-component 0.145.0 → 0.147.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 +1 -1
- package/src/molecules/ContentRow.jsx +31 -4
- package/src/molecules/ContentText.jsx +15 -1
- package/src/molecules/SectionCardItem.jsx +16 -6
- package/src/organisms/SectionCards.jsx +5 -1
- package/src/organisms/SectionNewsletter.jsx +9 -1
- package/src/utilities/ButtonGroup.jsx +13 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolkrabbi/kol-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.147.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",
|
|
@@ -10,8 +10,10 @@ import ContentText from './ContentText.jsx'
|
|
|
10
10
|
* thumb; article rides a 120px 16:9 thumb; work a framed 64px row; typeface a
|
|
11
11
|
* framed no-thumb block.
|
|
12
12
|
*
|
|
13
|
-
* @param {string} variant file | catalog | print | article | work | typeface
|
|
14
|
-
* (`default` is an alias of `file`)
|
|
13
|
+
* @param {string} variant file | catalog | print | article | work | typeface | roster
|
|
14
|
+
* (`default` is an alias of `file`). `roster` is the pickable
|
|
15
|
+
* row: filled tile, no border or divider, a FIXED 56px height the
|
|
16
|
+
* content fills, a 40px square thumb and two truncated lines.
|
|
15
17
|
* @param {ReactNode} media thumb content (omit → placeholder)
|
|
16
18
|
* @param {number|'fill'} thumb thumb edge px — overrides the ruled default; 0 hides; `'fill'` = a square
|
|
17
19
|
* the height of the row's content, whatever the rung (WorkListingRowsAndFilters)
|
|
@@ -61,6 +63,23 @@ const BOX = {
|
|
|
61
63
|
* transparent → fg-08 on hover ("0 → 16 is a big jump"). No md step: the rows
|
|
62
64
|
* were approved at their base values. */
|
|
63
65
|
showcase: { thumb: 'fill', ratio: '1 / 1', pad: S4, gap: S4, frame: 'transparent', frameHover: 'var(--kol-fg-08)', bg: 'var(--kol-surface-secondary)', minH: 168, align: 'items-stretch', thumbRadius: 'var(--kol-radius-xs)', thumbBorder: false },
|
|
66
|
+
/* ROSTER — a PICKABLE row (ContentRowRosterVariant, kol-chess 2026-08-31):
|
|
67
|
+
* filled tile, no border, no divider, fixed height, square thumb, two
|
|
68
|
+
* truncated lines that FILL the row rather than setting it.
|
|
69
|
+
*
|
|
70
|
+
* `file` is the nearest part and a different object: a bare ruled line with a
|
|
71
|
+
* 48px thumb, a divider, and no hover wash by the 2026-08-29 ruling — right
|
|
72
|
+
* for a file listing, wrong for a grid of things you choose between. These
|
|
73
|
+
* are targets, so they take a hover.
|
|
74
|
+
*
|
|
75
|
+
* THE FIXED HEIGHT IS THE POINT. Every other row in this family follows its
|
|
76
|
+
* content, and on a pick grid that reads as broken: the filer measured 34 →
|
|
77
|
+
* 40 → 50 → 58 across four passes, and ONE long meta line was enough to push
|
|
78
|
+
* a tile out of line with its neighbours. `height`, not `minH` — the content
|
|
79
|
+
* fills the row and cannot move it. */
|
|
80
|
+
roster: { thumb: 40, ratio: '1 / 1', pad: S2, gap: S2, height: 56, align: 'items-stretch',
|
|
81
|
+
bg: 'var(--kol-surface-secondary)', hover: 'var(--kol-fg-04)',
|
|
82
|
+
thumbBg: 'var(--kol-fg-04)', thumbBorder: false },
|
|
64
83
|
/* typeface's row is a COLUMN, not a line: a header (name/styles left,
|
|
65
84
|
* classification/year right) with a full-width specimen band under it. The
|
|
66
85
|
* shipped item is `flex-col gap-6`, and forcing it into the horizontal
|
|
@@ -148,6 +167,10 @@ export default function ContentRow({
|
|
|
148
167
|
* taller row carried `--kol-row-min-h: 224px !important`. */
|
|
149
168
|
'--kol-row-min-h': minHeight != null ? `${minHeight}px` : box.minH != null ? `${box.minH}px` : undefined,
|
|
150
169
|
'--kol-row-min-h-md': box.minHMd != null ? `${box.minHMd}px` : undefined,
|
|
170
|
+
/* A FIXED rung, not a floor — `roster` only. `minHeight` still overrides it,
|
|
171
|
+
* so a consumer that wants a taller pick row gets one; what it cannot get is
|
|
172
|
+
* a row whose height drifts with its own copy. */
|
|
173
|
+
'--kol-row-h': box.height != null ? `${minHeight ?? box.height}px` : undefined,
|
|
151
174
|
/* `fill` = the rung minus the vertical padding — a definite number, not a
|
|
152
175
|
* stretch (a min-height row has no definite cross size, so a stretched
|
|
153
176
|
* aspect-ratio square resolved to the image's intrinsic width).
|
|
@@ -211,7 +234,11 @@ export default function ContentRow({
|
|
|
211
234
|
</ContentMedia>
|
|
212
235
|
</div>
|
|
213
236
|
)}
|
|
214
|
-
|
|
237
|
+
{/* THE 2px IS DELIBERATE and the one off-grid value in `roster` (the filer
|
|
238
|
+
* ruled it on screen): the two lines stack to 34 inside a 40 content box,
|
|
239
|
+
* and pushing them fully apart puts the ascenders hard against the thumb's
|
|
240
|
+
* top and bottom edges. It is the ROW's ruling, so the row passes it. */}
|
|
241
|
+
<ContentText variant={variant} form="row" className={`flex-1 ${box.height != null ? 'py-[2px]' : ''}`.trim()} {...text} />
|
|
215
242
|
{/* `specs` rides the trailing edge on EVERY variant — year · material ·
|
|
216
243
|
* edition is a content difference, not a geometry one, and minting a
|
|
217
244
|
* seventh page-named box for it is the exact mistake §1 of the ticket is
|
|
@@ -234,7 +261,7 @@ export default function ContentRow({
|
|
|
234
261
|
)
|
|
235
262
|
|
|
236
263
|
const common = {
|
|
237
|
-
className: `kol-row group flex ${box.column ? 'flex-col' : ''} ${box.align} ${box.divider ? 'kol-row--divided' : ''} ${box.frame ? 'rounded-[var(--kol-radius-sm)] border' : ''} ${interactive ? 'cursor-pointer select-none' : ''} ${interactive && box.hover ? 'kol-content-hover' : ''} ${interactive && box.frameHover ? 'kol-content-hover-frame' : ''} ${className}`.trim(),
|
|
264
|
+
className: `kol-row group flex ${box.column ? 'flex-col' : ''} ${box.align} ${box.height != null ? 'kol-row--fixed' : ''} ${box.divider ? 'kol-row--divided' : ''} ${box.frame ? 'rounded-[var(--kol-radius-sm)] border' : ''} ${interactive ? 'cursor-pointer select-none' : ''} ${interactive && box.hover ? 'kol-content-hover' : ''} ${interactive && box.frameHover ? 'kol-content-hover-frame' : ''} ${className}`.trim(),
|
|
238
265
|
style: vars,
|
|
239
266
|
}
|
|
240
267
|
|
|
@@ -91,6 +91,15 @@ const RAMP = {
|
|
|
91
91
|
* ink, the year kol-mono-12 fg-64. Body unchanged (the site passes its face). */
|
|
92
92
|
row: { title: 'kol-mono-14 uppercase text-emphasis truncate', body: 'kol-sans-heading-03 leading-tight text-emphasis truncate', meta: 'kol-mono-14 text-emphasis', date: 'kol-mono-12 text-fg-64', tags: 'flex flex-wrap items-center gap-1.5' },
|
|
93
93
|
},
|
|
94
|
+
/* ROSTER — a pickable row's two lines, both TRUNCATED (ContentRowRosterVariant,
|
|
95
|
+
* kol-chess 2026-08-31). Written out rather than derived from `showcase`: the
|
|
96
|
+
* name-keyed maps below (FILL, GAPS) cannot be reached by a spread, and a
|
|
97
|
+
* derive that misses one of them is the exact trap `showcaseCanvas` fell into
|
|
98
|
+
* twice. Two literals beat a derive. Row form only — kol-chess wants no card,
|
|
99
|
+
* and a card nobody asked for is a variant to keep in step for nothing. */
|
|
100
|
+
roster: {
|
|
101
|
+
row: { title: 'kol-mono-14 text-fg-96 truncate', meta: 'kol-mono-12 text-fg-48 truncate' },
|
|
102
|
+
},
|
|
94
103
|
showcaseCanvas: {
|
|
95
104
|
/* RULED ON SCREEN (TypefaceCardAndRow, kol-website 2026-08-27): name and
|
|
96
105
|
* classification are FULL ink, the year steps to 64; the card's title is the
|
|
@@ -143,6 +152,8 @@ const ORDER = {
|
|
|
143
152
|
* item, which had been flattened into title-left / date-right and lost the
|
|
144
153
|
* styles line into a body slot below. */
|
|
145
154
|
showcaseCanvas: { card: ['title', 'body'], row: [['between', ['stack', 'title', 'body'], ['stack', 'detail', 'date']]] },
|
|
155
|
+
/* two lines, pushed apart by the column's own justify-between (FILL) */
|
|
156
|
+
roster: { row: ['title', 'meta'] },
|
|
146
157
|
}
|
|
147
158
|
|
|
148
159
|
/* inner line gap per variant/form, read from the shipped boxes and spelled in
|
|
@@ -158,7 +169,7 @@ const ORDER = {
|
|
|
158
169
|
* `self-stretch`, NOT `h-full` — the row carries `min-height`, never `height`,
|
|
159
170
|
* so `height: 100%` resolves against an indefinite parent, computes to auto,
|
|
160
171
|
* and shrink-wraps the column. Which is exactly what it did. */
|
|
161
|
-
const FILL = { showcase: true }
|
|
172
|
+
const FILL = { showcase: true, roster: true }
|
|
162
173
|
|
|
163
174
|
/* gap INSIDE a ['stack', …] block. Defaults to the tight 4px pair article
|
|
164
175
|
* wants; work's header stack is the shipped gap-1 md:gap-2. */
|
|
@@ -173,6 +184,9 @@ const GAPS = {
|
|
|
173
184
|
article: { card: 'var(--kol-spacing-3)', row: 'var(--kol-spacing-3)', hero: 'var(--kol-spacing-3)' },
|
|
174
185
|
showcase: { card: 'var(--kol-spacing-2)', row: 'var(--kol-spacing-4)' },
|
|
175
186
|
showcaseCanvas: { card: 'var(--kol-spacing-2)', row: 'var(--kol-spacing-6)' },
|
|
187
|
+
/* roster's lines are pushed apart by justify-between, so this is only a
|
|
188
|
+
* floor for the case where the row is given more height than 56. */
|
|
189
|
+
roster: { row: 'var(--kol-spacing-1)' },
|
|
176
190
|
}
|
|
177
191
|
|
|
178
192
|
/* `default` → `file` (user 2026-08-29) — kept working as an alias here too,
|
|
@@ -32,6 +32,10 @@ import { Icon } from '@kolkrabbi/kol-icons'
|
|
|
32
32
|
* @param {string} href link target; `http*`/`mailto` → new tab, else plain same-tab anchor
|
|
33
33
|
* @param {Function} onNavigate (event) => void — click seam on the same-tab anchor (SPA intercept)
|
|
34
34
|
* @param {'auto'|'9/6'|'10/6'|'16/9'|'1/1'} imageAspectRatio aspect class on the visual middle
|
|
35
|
+
* @param {number} zoom hover zoom scale for THIS card's visual (default 1.03, the shipped value).
|
|
36
|
+
* Per-feature because the right amount belongs to the artwork, not the component: 3% is correct on
|
|
37
|
+
* a dense photographic visual and invisible on sparse line-art, and one set can hold both
|
|
38
|
+
* (CardFeatureZoomScale, kol-website 2026-08-31).
|
|
35
39
|
* @param {string} imagePosition `<img>` object-position
|
|
36
40
|
*/
|
|
37
41
|
export default function SectionCardItem({
|
|
@@ -43,6 +47,7 @@ export default function SectionCardItem({
|
|
|
43
47
|
href,
|
|
44
48
|
onNavigate,
|
|
45
49
|
imageAspectRatio = 'auto',
|
|
50
|
+
zoom,
|
|
46
51
|
imagePosition = 'center',
|
|
47
52
|
className = '',
|
|
48
53
|
style,
|
|
@@ -89,9 +94,10 @@ export default function SectionCardItem({
|
|
|
89
94
|
{icon && <Icon name={icon} size={16} className="shrink-0" />}
|
|
90
95
|
</div>
|
|
91
96
|
|
|
92
|
-
{/* kol-card-feature-visual: zooms
|
|
93
|
-
*
|
|
94
|
-
*
|
|
97
|
+
{/* kol-card-feature-visual: zooms on card hover (chrome in kol-theme —
|
|
98
|
+
* CardFeatureHoverZoom 2026-08-12); all three visual forms ride the same
|
|
99
|
+
* wrapper, reduced-motion opts out. The AMOUNT is `zoom`, published as
|
|
100
|
+
* `--kol-card-feature-zoom` and defaulting to the shipped 1.03. */}
|
|
95
101
|
<div className={`kol-card-feature-visual w-full flex-auto flex items-center justify-center overflow-hidden ${aspectClass}`.trim()}>
|
|
96
102
|
{visual ? (
|
|
97
103
|
typeof visual === 'string' ? (
|
|
@@ -130,6 +136,10 @@ export default function SectionCardItem({
|
|
|
130
136
|
</>
|
|
131
137
|
)
|
|
132
138
|
|
|
139
|
+
/* the card publishes the amount; the theme rule reads it with 1.03 as the
|
|
140
|
+
* fallback, so a card that sets nothing renders exactly as it always did */
|
|
141
|
+
const rootStyle = zoom != null ? { ...style, '--kol-card-feature-zoom': zoom } : style
|
|
142
|
+
|
|
133
143
|
const baseClasses = textOnly
|
|
134
144
|
? `kol-card-feature kol-card-feature--text w-full flex-1 min-h-[180px] p-4 md:p-5 lg:p-6 gap-2 ${bg} rounded border border-fg-08 flex flex-col justify-between items-start overflow-hidden ${className}`
|
|
135
145
|
: `kol-card-feature w-full flex-1 h-[304px] md:h-72 p-4 md:p-6 gap-4 ${bg} rounded border border-fg-08 flex flex-col justify-between items-start overflow-hidden ${className}`.trim()
|
|
@@ -142,7 +152,7 @@ export default function SectionCardItem({
|
|
|
142
152
|
<a
|
|
143
153
|
href={href}
|
|
144
154
|
className={`${baseClasses} hover:border-fg-32 transition-colors duration-300`}
|
|
145
|
-
style={
|
|
155
|
+
style={rootStyle}
|
|
146
156
|
target="_blank"
|
|
147
157
|
rel="noreferrer noopener"
|
|
148
158
|
>
|
|
@@ -156,12 +166,12 @@ export default function SectionCardItem({
|
|
|
156
166
|
href={href}
|
|
157
167
|
onClick={onNavigate}
|
|
158
168
|
className={`${baseClasses} hover:border-fg-24 transition-colors duration-300`}
|
|
159
|
-
style={
|
|
169
|
+
style={rootStyle}
|
|
160
170
|
>
|
|
161
171
|
{content}
|
|
162
172
|
</a>
|
|
163
173
|
)
|
|
164
174
|
}
|
|
165
175
|
|
|
166
|
-
return <div className={baseClasses} style={
|
|
176
|
+
return <div className={baseClasses} style={rootStyle}>{content}</div>
|
|
167
177
|
}
|
|
@@ -22,7 +22,10 @@ import { minHeightClass } from './sectionHeights.js'
|
|
|
22
22
|
* paints its surface — every token inside resolves to the other theme's.
|
|
23
23
|
* @param {'full'|'80'|'60'|string} [height='60'] min-height on the family's ladder — full = 100dvh,
|
|
24
24
|
* 80 = 70svh / 80vh, 60 = 50svh / 60vh (default), 40 = 35svh / 40vh; content stays vertically centred inside it
|
|
25
|
-
* @param {{title, icon, visual, description, href, backgroundColor, imageAspectRatio}[]} features
|
|
25
|
+
* @param {{title, icon, visual, description, href, backgroundColor, imageAspectRatio, zoom}[]} features
|
|
26
|
+
* `zoom` is the per-card hover scale (default 1.03) — the right amount belongs to the ARTWORK,
|
|
27
|
+
* not the component: 3% reads correctly on a dense photographic visual and is invisible on sparse
|
|
28
|
+
* line-art, and one set can hold both (CardFeatureZoomScale, kol-website 2026-08-31).
|
|
26
29
|
* @param {ReactNode} eyebrow (alias label) · headline · body the header (heading-03 + mono lede by default)
|
|
27
30
|
* @param {ReactNode} actions centred action row under the cards
|
|
28
31
|
* @param {Function} onNavigate (event, feature) => void
|
|
@@ -97,6 +100,7 @@ export default function SectionCards({
|
|
|
97
100
|
href={feature.href}
|
|
98
101
|
backgroundColor={feature.backgroundColor}
|
|
99
102
|
imageAspectRatio={feature.imageAspectRatio}
|
|
103
|
+
zoom={feature.zoom}
|
|
100
104
|
onNavigate={onNavigate ? (event) => onNavigate(event, feature) : undefined}
|
|
101
105
|
className={itemClassName}
|
|
102
106
|
style={typeof itemStyle === 'function' ? itemStyle(index) : itemStyle}
|
|
@@ -51,6 +51,13 @@ import { minHeightClass } from './sectionHeights.js'
|
|
|
51
51
|
* Button (SectionNewsletterControlSize, kol-website 2026-08-31). The pair was hardcoded md with no
|
|
52
52
|
* seam, so a page that sets `size="lg"` on every other call-site button could not match it here and
|
|
53
53
|
* the newsletter read visibly smaller directly beneath them. Default is today's md — nothing moves.
|
|
54
|
+
* @param {boolean} [fullBleed=false] the FILL breaks the page gutter while the content keeps it
|
|
55
|
+
* (SectionNewsletterFullBleed, kol-website 2026-08-31). This card is a filled surface inside
|
|
56
|
+
* `.kol-page`, so the gutter clipped its background and left strips of page down both sides of the
|
|
57
|
+
* colour. Fill and content padding are the same box, so a consumer could not bleed one without
|
|
58
|
+
* dragging the other out with it. The breakout literal is SectionHero's, character for character —
|
|
59
|
+
* two organisms in one family must not invent two ways to leave a gutter. The section's own
|
|
60
|
+
* `px-5 sm:px-8` then re-insets the content, so only the fill moves.
|
|
54
61
|
* @param {string} className extra classes on the section
|
|
55
62
|
* @param {'primary'|'secondary'|'tertiary'|'inverse'|'auto'|'none'|string} background the section's surface
|
|
56
63
|
* (SectionBackgroundProp, 2026-08-27) — a named surface, `none`, or a raw utility / token string; default = what it painted before
|
|
@@ -58,6 +65,7 @@ import { minHeightClass } from './sectionHeights.js'
|
|
|
58
65
|
export default function SectionNewsletter({
|
|
59
66
|
height = '40',
|
|
60
67
|
controlSize = 'md',
|
|
68
|
+
fullBleed = false,
|
|
61
69
|
theme,
|
|
62
70
|
background,
|
|
63
71
|
eyebrow,
|
|
@@ -118,7 +126,7 @@ export default function SectionNewsletter({
|
|
|
118
126
|
* boundary and read as breaking out of it. `px-5` is a floor the band owns.
|
|
119
127
|
* Desktop does not move — the measure caps below the padded width, so the
|
|
120
128
|
* inner block still centres at 80px from the band edge. */
|
|
121
|
-
className={`kol-section-newsletter w-full flex flex-col justify-center px-5 sm:px-8 py-24 ${surfaceClass(background, theme ? 'primary' : 'none')} ${theme ? 'text-auto' : ''} ${minHeightClass(height)} ${className}`.replace(/\s+/g, ' ').trim()}
|
|
129
|
+
className={`kol-section-newsletter ${fullBleed ? 'w-screen ml-[calc(50%-50vw)]' : 'w-full'} flex flex-col justify-center px-5 sm:px-8 py-24 ${surfaceClass(background, theme ? 'primary' : 'none')} ${theme ? 'text-auto' : ''} ${minHeightClass(height)} ${className}`.replace(/\s+/g, ' ').trim()}
|
|
122
130
|
>
|
|
123
131
|
{/* the family's ONE cap — the shell's --kol-container-max ladder — and
|
|
124
132
|
* inside it the lede's MEASURE on a wrapper (SectionNewsletterForm,
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @param {'left'|'center'|'right'} align horizontal alignment + container layout mode (default 'center')
|
|
12
12
|
* @param {string} title optional heading rendered above the group, authored at the call site
|
|
13
|
-
* @param {string} className extra container classes (appended)
|
|
13
|
+
* @param {string} className extra container classes (appended) — note this lands on the OUTER
|
|
14
|
+
* container, not the flex row that carries the gap; the gap is responsive by default (8 stacked,
|
|
15
|
+
* 16 as a row) rather than reachable, so there is nothing to reach for.
|
|
14
16
|
* @param {ReactNode} children the Buttons
|
|
15
17
|
*/
|
|
16
18
|
|
|
@@ -32,7 +34,16 @@ export default function ButtonGroup({ align = 'center', title, className = '', c
|
|
|
32
34
|
{title && <h3 className="kol-sans-heading-05 text-emphasis mb-6">{title}</h3>}
|
|
33
35
|
<div
|
|
34
36
|
className={[
|
|
35
|
-
|
|
37
|
+
/* THE GAP IS TWO JOBS, NOT ONE (ButtonGroupResponsiveGap, kol-website
|
|
38
|
+
* 2026-08-31). The group changes axis at `sm`, so a single `gap-4` was
|
|
39
|
+
* doing horizontal separation between two side-by-side buttons AND
|
|
40
|
+
* vertical separation between two full-width stacked ones. Those do not
|
|
41
|
+
* want the same number: 16 reads too open stacked (user: "16 is way too
|
|
42
|
+
* big, at least lets see 8 or 12"), and 8 is what looks right on device.
|
|
43
|
+
* A responsive default rather than a prop — the stacked case IS the
|
|
44
|
+
* narrow viewport, so the value is pickable once instead of per
|
|
45
|
+
* consumer. The row keeps today's 16 and nothing moves at `sm` and up. */
|
|
46
|
+
'flex flex-col gap-2 sm:flex-row sm:gap-4 sm:items-center',
|
|
36
47
|
JUSTIFY[align] || JUSTIFY.center,
|
|
37
48
|
].join(' ')}
|
|
38
49
|
>
|