@kolkrabbi/kol-component 0.146.0 → 0.148.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.148.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",
|
|
@@ -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
|
}
|
|
@@ -40,7 +40,21 @@ import usePrefersReducedMotion from '../hooks/usePrefersReducedMotion.js'
|
|
|
40
40
|
* a class built at runtime is never emitted.
|
|
41
41
|
*
|
|
42
42
|
* @param {string} form 'grid' | 'list'
|
|
43
|
-
* @param {string} min grid track minimum, card form (default 320px)
|
|
43
|
+
* @param {string} min grid track minimum, card form (default 320px) — the FLUID wall's floor
|
|
44
|
+
* @param {string} minCol the floor a `cols` track may not go under. DEFAULTS TO `min` (320px), so
|
|
45
|
+
* the count path and the fluid path share one ruled minimum and raising
|
|
46
|
+
* `min` raises both (ContentCollectionMinColumnWidth, kol-chess
|
|
47
|
+
* 2026-08-31). `cols` is a CEILING now, not a command: the wall takes up
|
|
48
|
+
* to N columns and drops one rather than let a track go under this.
|
|
49
|
+
*
|
|
50
|
+
* WHY NOT A HIGHER DEFAULT. The filer measured a roster row clipping on
|
|
51
|
+
* 7 of 10 rows at 324 and 3 of 10 at 373, which argues for ~360 — but that
|
|
52
|
+
* is a number about a ROW two truncated lines tall, and this floor governs
|
|
53
|
+
* every kind. 320 is the width this DS has already ruled as the narrowest
|
|
54
|
+
* acceptable track and lived with; applying it to the `cols` path is
|
|
55
|
+
* carrying an existing ruling across, where 360 would be a new estate-wide
|
|
56
|
+
* law made from one page's evidence. A wall whose content needs more says
|
|
57
|
+
* so: `minCol="360px"`.
|
|
44
58
|
* @param {number|object} cols OPT-IN column count, grid form. A number: 1 below
|
|
45
59
|
* md, N from md (1–6). A map per breakpoint
|
|
46
60
|
* (`{ md: 3, xl: 4 }` — sm · md · lg · xl · 2xl): 1
|
|
@@ -59,23 +73,34 @@ import usePrefersReducedMotion from '../hooks/usePrefersReducedMotion.js'
|
|
|
59
73
|
* @param {boolean} stagger enter animation on/off (reduced motion wins)
|
|
60
74
|
*/
|
|
61
75
|
/* literal per rung × count — a class built at runtime is never emitted */
|
|
76
|
+
/* THE COUNT IS A CEILING NOW, NOT A COMMAND (ContentCollectionMinColumnWidth,
|
|
77
|
+
* kol-chess 2026-08-31). `cols` used to emit `grid-cols-N` and take N columns
|
|
78
|
+
* whatever they measured, which is how a WIDER screen came to clip MORE text:
|
|
79
|
+
* kol-chess's roster ran 1 column at 350 on a phone and 2 columns at 324 at
|
|
80
|
+
* 768 — narrower tracks on a bigger viewport, 7 rows of 10 clipping at both.
|
|
81
|
+
*
|
|
82
|
+
* So the rungs publish a VARIABLE instead of a track list, and one static
|
|
83
|
+
* template below turns it into "at most N, and never narrower than the floor".
|
|
84
|
+
* Literal class strings, as before — a class built at runtime is never emitted
|
|
85
|
+
* by Tailwind's scanner, and arbitrary-property utilities are no exception. */
|
|
62
86
|
const COLS_AT = {
|
|
63
|
-
sm: { 1: 'sm:
|
|
64
|
-
md: { 1: 'md:
|
|
65
|
-
lg: { 1: 'lg:
|
|
66
|
-
xl: { 1: 'xl:
|
|
67
|
-
'2xl': { 1: '2xl:
|
|
87
|
+
sm: { 1: 'sm:[--kol-wall-cols:1]', 2: 'sm:[--kol-wall-cols:2]', 3: 'sm:[--kol-wall-cols:3]', 4: 'sm:[--kol-wall-cols:4]', 5: 'sm:[--kol-wall-cols:5]', 6: 'sm:[--kol-wall-cols:6]' },
|
|
88
|
+
md: { 1: 'md:[--kol-wall-cols:1]', 2: 'md:[--kol-wall-cols:2]', 3: 'md:[--kol-wall-cols:3]', 4: 'md:[--kol-wall-cols:4]', 5: 'md:[--kol-wall-cols:5]', 6: 'md:[--kol-wall-cols:6]' },
|
|
89
|
+
lg: { 1: 'lg:[--kol-wall-cols:1]', 2: 'lg:[--kol-wall-cols:2]', 3: 'lg:[--kol-wall-cols:3]', 4: 'lg:[--kol-wall-cols:4]', 5: 'lg:[--kol-wall-cols:5]', 6: 'lg:[--kol-wall-cols:6]' },
|
|
90
|
+
xl: { 1: 'xl:[--kol-wall-cols:1]', 2: 'xl:[--kol-wall-cols:2]', 3: 'xl:[--kol-wall-cols:3]', 4: 'xl:[--kol-wall-cols:4]', 5: 'xl:[--kol-wall-cols:5]', 6: 'xl:[--kol-wall-cols:6]' },
|
|
91
|
+
'2xl': { 1: '2xl:[--kol-wall-cols:1]', 2: '2xl:[--kol-wall-cols:2]', 3: '2xl:[--kol-wall-cols:3]', 4: '2xl:[--kol-wall-cols:4]', 5: '2xl:[--kol-wall-cols:5]', 6: '2xl:[--kol-wall-cols:6]' },
|
|
68
92
|
}
|
|
69
93
|
const colsClasses = (cols) => {
|
|
70
94
|
const map = typeof cols === 'number' ? { md: cols } : cols
|
|
71
95
|
if (!map || typeof map !== 'object') return ''
|
|
72
96
|
const rungs = Object.keys(COLS_AT).map((bp) => COLS_AT[bp][map[bp]]).filter(Boolean)
|
|
73
|
-
return rungs.length ? ['
|
|
97
|
+
return rungs.length ? ['[--kol-wall-cols:1]', ...rungs].join(' ') : ''
|
|
74
98
|
}
|
|
75
99
|
|
|
76
100
|
export default function ContentCollection({
|
|
77
101
|
form = 'grid',
|
|
78
102
|
min = '320px',
|
|
103
|
+
minCol,
|
|
79
104
|
cols,
|
|
80
105
|
listMin,
|
|
81
106
|
gap,
|
|
@@ -103,9 +128,18 @@ export default function ContentCollection({
|
|
|
103
128
|
* wall is one, so a row inside it steps on the wall's own width */
|
|
104
129
|
containerType: 'inline-size',
|
|
105
130
|
display: 'grid',
|
|
106
|
-
/*
|
|
131
|
+
/* the classes no longer carry tracks — they set `--kol-wall-cols` and this
|
|
132
|
+
* ONE static template reads it, so the count and the floor cannot disagree */
|
|
107
133
|
gridTemplateColumns: colsCls
|
|
108
|
-
|
|
134
|
+
/* AT MOST N, AND NEVER NARROWER THAN THE FLOOR. `auto-fill` counts the
|
|
135
|
+
* tracks; the track size is the LARGER of the floor and an even 1/N
|
|
136
|
+
* share, so the wall takes N columns while they fit and drops one the
|
|
137
|
+
* moment a share would go under the floor. `min(100%, …)` is the guard
|
|
138
|
+
* that keeps a single narrow container from overflowing — without it a
|
|
139
|
+
* 350px phone gets one 360px track and a horizontal scrollbar.
|
|
140
|
+
* All of it is CSS: no measurement, no observer, and it works inside
|
|
141
|
+
* the container query this wall already establishes. */
|
|
142
|
+
? `repeat(auto-fill, minmax(min(100%, max(${minCol ?? min}, calc((100% - (var(--kol-wall-cols, 1) - 1) * ${g}) / var(--kol-wall-cols, 1)))), 1fr))`
|
|
109
143
|
: form === 'list'
|
|
110
144
|
/* minmax(0, 1fr), never a bare 1fr (= minmax(auto, 1fr)): a truncated
|
|
111
145
|
* nowrap line handed its min-content width to the track and a /work row
|
|
@@ -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
|
>
|